Compare commits

16 Commits

29 changed files with 1027 additions and 166 deletions

2
.gitignore vendored
View File

@@ -2,8 +2,6 @@
.envrc
result
example.json
go.sum
go.mod
./gokill
output.md
thoughts.md

View File

@@ -1,4 +1,22 @@
# gokill
gokill is aimed at activists, journalists and others that need to protect their data against access under all circumstances.
gokill falls under the category of anti-forensic tools, helping you to protect yourself against repression.
It is built for worst case scenarios when intruders physical gaining access to a device.
In such heated situations gokill helps you automatically executing tasks like:
- locking the screen
- sending a chat message
- deleting data
- encrypting partitions
- destroying encrypted partitions
- and many more
the tasks gokill executes could be done by hand using shellscripts, cronjobs, daemons ect.
but that means everyone needs to figure it out for themselves, and eventually make mistakes.
the idea of gokill is to provide a wide variarity of possibilities out of the box while making sure they are well tested.
gokill aims to be highly configurable and easily extendable.
'gokill' is a tool that completes some actions when a certain event occurs.
actions can vary from shuting down the machine to sending mails over erasing data.
actions can be triggert by certain conditions like specific outcomes of unix

View File

@@ -36,7 +36,7 @@ func (a StagedActions) executeInternal(f func(Action)) {
continue
}
fmt.Printf("Execute Stage %v\n", idx+1)
internal.Log.Infof("Execute Stage %v", idx+1)
for actionidx, _ := range stage.Actions {
go f(stage.Actions[actionidx])
}
@@ -45,7 +45,7 @@ func (a StagedActions) executeInternal(f func(Action)) {
err := <-a.ActionChan
if err != nil {
fmt.Printf("Error occured on Stage %d: %s\n", idx+1, err)
internal.Log.Errorf("Error occured on Stage %d: %s", idx+1, err)
}
}
}
@@ -119,11 +119,13 @@ func NewAction(config []internal.ActionConfig) (Action, error) {
func GetAllActions() []DocumentedAction {
return []DocumentedAction{
Printer{},
TimeOut{},
Command{},
Printer{},
ShellScript{},
Shutdown{},
SendMatrix{},
SendTelegram{},
TimeOut{},
}
}

View File

@@ -2,7 +2,6 @@ package actions
import (
"encoding/json"
"fmt"
"unknown.com/gokill/internal"
)
@@ -13,12 +12,12 @@ type Printer struct {
}
func (p Printer) Execute() {
fmt.Printf("Print action fires. Message: %s\n", p.Message)
internal.LogDoc(p).Infof("Print action fires. Message: %s", p.Message)
p.ActionChan <- nil
}
func (p Printer) DryExecute() {
fmt.Printf("Print action fire test. Message: %s\n", p.Message)
internal.LogDoc(p).Infof("Print action fire test. Message: %s", p.Message)
p.ActionChan <- nil
}

226
actions/send_matrix.go Normal file
View File

@@ -0,0 +1,226 @@
package actions
import (
"fmt"
"encoding/json"
"context"
"errors"
"sync"
"time"
_ "github.com/mattn/go-sqlite3"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/crypto/cryptohelper"
"unknown.com/gokill/internal"
)
type SendMatrix struct {
Homeserver string `json:"homeserver"`
Username string `json:"username"`
Password string `json:"password"`
Database string `json:"database"`
RoomId string `json:"roomId"`
Message string `json:"message"`
TestMessage string `json:"testMessage"`
ActionChan ActionResultChan
}
func (s SendMatrix) sendMessage(message string) error {
client, err := mautrix.NewClient(s.Homeserver, "", "")
if err != nil {
return err
}
cryptoHelper, err := cryptohelper.NewCryptoHelper(client, []byte("meow"), s.Database)
if err != nil {
return err
}
cryptoHelper.LoginAs = &mautrix.ReqLogin{
Type: mautrix.AuthTypePassword,
Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: s.Username},
Password: s.Password,
}
err = cryptoHelper.Init()
if err != nil {
return err
}
client.Crypto = cryptoHelper
internal.LogDoc(s).Info("Matrix Client Now running")
syncCtx, cancelSync := context.WithCancel(context.Background())
var syncStopWait sync.WaitGroup
syncStopWait.Add(1)
go func() {
err = client.SyncWithContext(syncCtx)
defer syncStopWait.Done()
if err != nil && !errors.Is(err, context.Canceled) {
return
}
}()
time.Sleep(5 * time.Second)
resp, err := client.SendText(id.RoomID(s.RoomId), message)
if err != nil {
return fmt.Errorf("Failed to send event")
} else {
internal.LogDoc(s).Info("Matrix Client: Message sent")
internal.LogDoc(s).Infof("Matrix Client: event_id: %s", resp.EventID.String())
}
cancelSync()
syncStopWait.Wait()
err = cryptoHelper.Close()
if err != nil {
return fmt.Errorf("Error closing database")
}
return nil
}
func (s SendMatrix) DryExecute() {
internal.LogDoc(s).Info("SendMatrix: Trying to send test message")
err := s.sendMessage(s.TestMessage)
if err != nil {
internal.LogDoc(s).Info("SendMatrix: failed to send test message")
}
s.ActionChan <- err
}
func (s SendMatrix) Execute() {
internal.LogDoc(s).Info("SendMatrix: Trying to send message")
err := s.sendMessage(s.Message)
if err != nil {
internal.LogDoc(s).Info("SendMatrix: failed to send message")
}
s.ActionChan <- err
}
func CreateSendMatrix(config internal.ActionConfig, c ActionResultChan) (SendMatrix, error) {
result := SendMatrix{}
err := json.Unmarshal(config.Options, &result)
if err != nil {
return SendMatrix{}, err
}
if result.Homeserver == "" {
return SendMatrix{}, internal.OptionMissingError{"homeserver"}
}
if result.Username == "" {
return SendMatrix{}, internal.OptionMissingError{"username"}
}
if result.Password == "" {
return SendMatrix{}, internal.OptionMissingError{"password"}
}
if result.Database == "" {
return SendMatrix{}, internal.OptionMissingError{"database"}
}
if result.RoomId == "" {
return SendMatrix{}, internal.OptionMissingError{"roomId"}
}
if result.Message == "" {
return SendMatrix{}, internal.OptionMissingError{"message"}
}
if result.TestMessage == "" {
return SendMatrix{}, internal.OptionMissingError{"testMessage"}
}
result.ActionChan = c
return result, nil
}
func (s SendMatrix) Create(config internal.ActionConfig, c ActionResultChan) (Action, error) {
return CreateSendMatrix(config, c)
}
func (p SendMatrix) GetName() string {
return "SendMatrix"
}
func (p SendMatrix) GetDescription() string {
return "Sends a message to a given room. The user needs to be part of that room already."
}
func (p SendMatrix) GetExample() string {
return `
{
"type": "SendMatrix",
"options": {
"homeserver": "matrix.org",
"username": "testuser",
"password": "super-secret",
"database": "/etc/gokill/matrix.db",
"roomId": "!Balrthajskensaw:matrix.org",
"message": "attention, intruders got my device!",
"testMessage": "this is just a test, no worries"
}
}
`
}
func (p SendMatrix) GetOptions() []internal.ConfigOption {
return []internal.ConfigOption{
{
Name: "homeserver",
Type: "string",
Description: "homeserver address.",
Default: "",
},
{
Name: "username",
Type: "string",
Description: "username (localpart, wihout homeserver address)",
Default: "",
},
{
Name: "password",
Type: "string",
Description: "password in clear text",
Default: "",
},
{
Name: "database",
Type: "string",
Description: "path to database file, will be created if not existing. this is necessary to sync keys for encryption.",
Default: "",
},
{
Name: "roomId",
Type: "string",
Description: "",
Default: "",
},
{
Name: "message",
Type: "string",
Description: "actual message that should be sent",
Default: "",
},
{
Name: "testMessage",
Type: "string",
Description: "message sent during test run",
Default: "",
},
}
}

148
actions/send_telegram.go Normal file
View File

@@ -0,0 +1,148 @@
package actions
import (
"fmt"
"encoding/json"
//"strconv"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"unknown.com/gokill/internal"
)
type SendTelegram struct {
Token string `json:"token"`
ChatId int64 `json:"chatId"`
Message string `json:"message"`
TestMessage string `json:"testMessage"`
ActionChan ActionResultChan
}
func (s SendTelegram) sendMessage(message string) error {
bot, err := tgbotapi.NewBotAPI(s.Token)
if err != nil {
return fmt.Errorf("SendTelegram sendMessage error: %s", err)
}
bot.Debug = false
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
chatId := s.ChatId
msg := tgbotapi.NewMessage(chatId, message)
_, err = bot.Send(msg)
if err != nil {
return fmt.Errorf("SendTelegram sendMessage error: %s", err)
}
return nil
}
func (s SendTelegram) DryExecute() {
internal.LogDoc(s).Info("SendTelegram: Trying to send test message")
err := s.sendMessage(s.TestMessage)
if err != nil {
internal.LogDoc(s).Info("SendTelegram: failed to send test message")
}
s.ActionChan <- err
}
func (s SendTelegram) Execute() {
internal.LogDoc(s).Info("SendTelegram: Trying to send message")
err := s.sendMessage(s.Message)
if err != nil {
internal.LogDoc(s).Info("SendTelegram: failed to send message")
}
s.ActionChan <- err
}
func CreateSendTelegram(config internal.ActionConfig, c ActionResultChan) (SendTelegram, error) {
result := SendTelegram{
ChatId: 0,
}
err := json.Unmarshal(config.Options, &result)
if err != nil {
return SendTelegram{}, err
}
if result.Token == "" {
return SendTelegram{}, internal.OptionMissingError{"token"}
}
if result.ChatId == 0 {
return SendTelegram{}, internal.OptionMissingError{"chatId"}
}
if result.Message == "" {
return SendTelegram{}, internal.OptionMissingError{"message"}
}
if result.TestMessage == "" {
return SendTelegram{}, internal.OptionMissingError{"testMessage"}
}
result.ActionChan = c
return result, nil
}
func (s SendTelegram) Create(config internal.ActionConfig, c ActionResultChan) (Action, error) {
return CreateSendTelegram(config, c)
}
func (p SendTelegram) GetName() string {
return "SendTelegram"
}
func (p SendTelegram) GetDescription() string {
return "Sends a message to a given room. The user needs to be part of that room already."
}
func (p SendTelegram) GetExample() string {
return `
{
"type": "SendTelegram",
"options": {
"token": "5349923487:FFGrETxa0pA29d02Akslw-lkwjdA92KAH2",
"chatId": -832345892,
"message": "attention, intruders got my device!",
"testMessage": "this is just a test, no worries"
}
}
`
}
func (p SendTelegram) GetOptions() []internal.ConfigOption {
return []internal.ConfigOption{
{
Name: "token",
Type: "string",
Description: "telegram bot token (ask botfather)",
Default: "",
},
{
Name: "chatId",
Type: "int",
Description: "chatId of group or chat you want the message be sent to.",
Default: "",
},
{
Name: "message",
Type: "string",
Description: "actual message that should be sent",
Default: "",
},
{
Name: "testMessage",
Type: "string",
Description: "message sent during test run",
Default: "",
},
}
}

View File

@@ -18,7 +18,6 @@ func isExecutableFile(path string) bool {
fi, err := os.Lstat(path)
if err != nil {
fmt.Println("Test executing Shellscript Failed.")
return false
}
@@ -33,18 +32,18 @@ func isExecutableFile(path string) bool {
}
func (c ShellScript) DryExecute() {
fmt.Printf("Test Executing ShellScript:\n%s\n", c.Path)
internal.LogDoc(c).Infof("Test Executing ShellScript:\n%s", c.Path)
_, err := os.Open(c.Path)
if err != nil {
fmt.Println("Test executing Shellscript Failed.")
internal.LogDoc(c).Warning("Test executing Shellscript Failed.")
c.ActionChan <- err
return
}
if !isExecutableFile(c.Path) {
fmt.Println("Test executing Shellscript Failed.")
internal.LogDoc(c).Warning("Test executing Shellscript Failed.")
c.ActionChan <- fmt.Errorf("File is not executable: %s", c.Path)
return
}
@@ -54,7 +53,7 @@ func (c ShellScript) DryExecute() {
func (c ShellScript) Execute() {
if !isExecutableFile(c.Path) {
fmt.Println("Test executing Shellscript Failed.")
internal.LogDoc(c).Warning("Executing Shellscript Failed.")
c.ActionChan <- fmt.Errorf("File is not executable: %s", c.Path)
return
}
@@ -64,11 +63,11 @@ func (c ShellScript) Execute() {
stdout, err := cmd.Output()
if err != nil {
fmt.Println(err.Error())
c.ActionChan <- err
internal.LogDoc(c).Warning("Failed to execute Shellscript")
c.ActionChan <- fmt.Errorf("Error during ShellScript execute: %s", err)
}
fmt.Println(string(stdout[:]))
internal.LogDoc(c).Infof("Shellscript output:\n%s", string(stdout[:]))
c.ActionChan <- nil
}

View File

@@ -1,7 +1,6 @@
package actions
import (
"fmt"
"os/exec"
"encoding/json"
@@ -14,27 +13,30 @@ type Shutdown struct {
}
func (s Shutdown) DryExecute() {
fmt.Printf("shutdown -h %s\n", s.Timeout)
fmt.Println("Test Shutdown executed...")
internal.LogDoc(s).Infof("shutdown -h %s", s.Timeout)
internal.LogDoc(s).Info("Test Shutdown executed...")
s.ActionChan <- nil
}
func (s Shutdown) Execute() {
if err := exec.Command("shutdown", "-h", s.Timeout).Run(); err != nil {
fmt.Println("Failed to initiate shutdown:", err)
internal.LogDoc(s).Errorf("Failed to initiate shutdown: %s", err)
}
fmt.Println("Shutdown executed...")
internal.LogDoc(s).Notice("Shutdown executed...")
s.ActionChan <- nil
}
func (s Shutdown) Create(config internal.ActionConfig, c ActionResultChan) (Action, error) {
var result Shutdown
result := Shutdown{
Timeout: "now",
}
err := json.Unmarshal(config.Options, &result)
if err != nil {
fmt.Println("Parsing Shutdown options failed.")
internal.LogDoc(s).Warning("Parsing Shutdown options failed.")
return Shutdown{}, err
}

View File

@@ -2,7 +2,6 @@ package actions
import (
"encoding/json"
"fmt"
"time"
"unknown.com/gokill/internal"
@@ -18,7 +17,7 @@ func (t TimeOut) DryExecute() {
}
func (t TimeOut) Execute() {
fmt.Printf("Waiting %d seconds\n", t.Duration)
internal.LogDoc(t).Infof("Waiting %d seconds", t.Duration)
time.Sleep(time.Duration(t.Duration) * time.Second)
t.ActionChan <- nil
}

View File

@@ -24,19 +24,19 @@ func isCommandAvailable(name string) bool {
}
func (c Command) DryExecute() {
fmt.Printf("Test Executing Command:\n%s\n", c.Command)
internal.LogDoc(c).Infof("Test Executing Command: %s", c.Command)
command, _, err := c.splitCommandString()
if err != nil {
fmt.Printf("Error during argument parsing of command '%s'\n", c.Command)
fmt.Println(err)
internal.LogDoc(c).Errorf("Error during argument parsing of command '%s'", c.Command)
c.ActionChan <- fmt.Errorf("Error on Command action: %s", err)
return
}
isAvailable := isCommandAvailable(command)
if !isAvailable {
fmt.Printf("Command %s not found\n", command)
internal.LogDoc(c).Warningf("Command %s not found", command)
c.ActionChan <- fmt.Errorf("Command %s not found!", command)
return
}
@@ -60,7 +60,7 @@ func (c Command) splitCommandString() (string, []string, error) {
func (c Command) Execute() {
command, args, err := c.splitCommandString()
fmt.Println("Executing command: ", c.Command)
internal.LogDoc(c).Infof("Executing command: ", c.Command)
if err != nil {
c.ActionChan <- err
@@ -72,11 +72,11 @@ func (c Command) Execute() {
stdout, err := cmd.Output()
if err != nil {
fmt.Println(err.Error())
c.ActionChan <- err
internal.LogDoc(c).Errorf("%s", err.Error())
c.ActionChan <- fmt.Errorf("Executing Command '%s' failed: %s", c.Command, err)
}
fmt.Println(string(stdout[:]))
internal.LogDoc(c).Infof("Command Output:\n%s", string(stdout[:]))
c.ActionChan <- nil
}

View File

@@ -37,7 +37,7 @@ func writeToFile(path string, documenter internal.Documenter) error {
f, err := os.Create(fileName)
if err != nil {
fmt.Println(err)
internal.Log.Errorf("Error during writeToFile: %s", err)
return err
}
@@ -46,7 +46,7 @@ func writeToFile(path string, documenter internal.Documenter) error {
_, err = f.WriteString(getMarkdown(documenter))
if err != nil {
fmt.Println(err)
internal.Log.Errorf("Error during writeToFile: %s", err)
return err
}
@@ -86,9 +86,12 @@ func printDocumentersSummary() {
func main() {
outputPath := flag.String("output", "", "path where docs/ shoud be created")
verbose := flag.Bool("verbose", false, "log debug information")
flag.Parse()
internal.InitLogger()
internal.SetVerbose(*verbose)
if *outputPath == "" {
printDocumentersSummary()
return

View File

@@ -37,19 +37,24 @@ func GetDocumentation() string {
}
func main() {
configFilePath := flag.String("c", "", "path to config file")
showDoc := flag.Bool("d", false, "show doc")
testRun := flag.Bool("t", false, "test run")
verbose := flag.Bool("verbose", false, "log debug info")
flag.Parse()
internal.InitLogger()
internal.SetVerbose(*verbose)
if *showDoc {
fmt.Print(GetDocumentation())
return
}
if *configFilePath == "" {
fmt.Println("No config file given. Use --help to show usage.")
internal.Log.Warning("No config file given. Use --help to show usage.")
return
}
@@ -58,7 +63,7 @@ func main() {
configFile, err := os.ReadFile(*configFilePath)
if err != nil {
fmt.Println("Error loading config file: ", err)
internal.Log.Errorf("Error loading config file: %s", err)
return
}
@@ -66,7 +71,7 @@ func main() {
err = json.Unmarshal(configFile, &f)
if err != nil {
fmt.Println(err)
internal.Log.Errorf("Error pasing json file: %s", err)
return
}
@@ -75,7 +80,7 @@ func main() {
trigger, err := triggers.NewTrigger(cfg)
if err != nil {
fmt.Println(err)
internal.Log.Errorf("%s", err)
return
}

BIN
docbuilder Executable file

Binary file not shown.

18
etc/gokill/config.json Normal file
View File

@@ -0,0 +1,18 @@
[
{
"type": "Timeout",
"name": "example trigger",
"options": {
"duration": 5
},
"actions": [
{
"type": "Print",
"options": {
"message": "hello world"
}
}
]
}
]

36
flake.lock generated
View File

@@ -18,7 +18,41 @@
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
"nixpkgs": "nixpkgs",
"utils": "utils"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1694529238,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},

223
flake.nix
View File

@@ -4,133 +4,137 @@
#nixpkgs for testing framework
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
outputs = { self, nixpkgs, ... }:
inputs.utils.url = "github:numtide/flake-utils";
outputs = { self, nixpkgs, utils, ... }:
nixpkgs.lib.attrsets.recursiveUpdate
(utils.lib.eachSystem (utils.lib.defaultSystems) ( system:
let
forAllSystems = nixpkgs.lib.genAttrs [ "x86_64-linux" ];
pkgs = nixpkgs.legacyPackages."x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
currentVendorHash = "sha256-Q14p7L2Ez/kvBhMUxlyMA1I/XEIxgSXOp4dpmH/SQyI=";
in
{
devShell."x86_64-linux" = pkgs.mkShell {
devShells.default = pkgs.mkShell {
packages = with pkgs; [
go
gotools
mdbook
olm
dpkg
];
};
packages.x86_64-linux.gokill = nixpkgs.legacyPackages.x86_64-linux.buildGoModule rec {
pname = "gokill";
version = "1.0";
vendorHash = "sha256-aKEOMeW9QVSLsSuHV4b1khqM0rRrMjJ6Eu5RjY+6V8k=";
src = ./.;
packages = rec {
gokill = pkgs.buildGoModule rec {
pname = "gokill";
version = "1.0";
vendorHash = currentVendorHash;
src = ./.;
postInstall = ''
'';
};
buildInputs = [
pkgs.olm
];
packages.x86_64-linux.gokill-docbuilder = nixpkgs.legacyPackages.x86_64-linux.buildGoModule rec {
pname = "docbuilder";
version = "1.0";
vendorHash = null;
buildFLags = "-o . $dest/cmd/gokill/docbuilder";
src = ./.;
postInstall = ''
cp -r ./etc $out/ #for .deb packages
'';
};
postInstall = ''
'';
};
gokill-docbuilder = pkgs.buildGoModule rec {
pname = "docbuilder";
version = "1.0";
vendorHash = currentVendorHash;
buildFLags = "-o . $dest/cmd/gokill/docbuilder";
src = ./.;
buildInputs = [
pkgs.olm
];
packages.x86_64-linux.docs = pkgs.callPackage (import ./docs/default.nix) { self = self; };
postInstall = ''
'';
};
packages.x86_64-linux.default = self.packages.x86_64-linux.gokill;
nixosModules.gokill = { config, lib, pkgs, ... }:
let
cfg = config.services.gokill;
configFile = pkgs.writeText "config.json" (builtins.toJSON cfg.triggers);
gokill-pkg = self.packages.x86_64-linux.gokill;
in
{
options = {
services.gokill = {
enable = lib.mkOption {
default = false;
type = lib.types.bool;
description = lib.mdDoc ''
Enables gokill daemon
'';
};
triggers = lib.mkOption {
description = "list of triggers";
default = [];
type = with lib.types; lib.types.listOf ( submodule {
options = {
type = lib.mkOption {
type = lib.types.str;
};
name = lib.mkOption {
type = lib.types.str;
};
options = lib.mkOption {
type = lib.types.attrs;
};
actions = lib.mkOption {
description = "list of actions";
type = with lib.types; lib.types.listOf ( submodule {
options = {
type = lib.mkOption {
type = lib.types.str;
};
options = lib.mkOption {
type = lib.types.attrs;
};
stage = lib.mkOption {
type = lib.types.int;
};
};
});
};
};
});
};
extraConfig = lib.mkOption {
type = lib.types.str;
description = lib.mdDoc ''
gokill config.json
'';
};
gokillSnap = pkgs.snapTools.makeSnap {
meta = {
name = "gokill";
summary = "simple but efficient";
description = "this should be longer";
architectures = [ "amd64" ];
confinement = "classic";
apps.gokill.command = "${gokill}/bin/gokill";
};
};
config = lib.mkIf cfg.enable {
systemd.services.gokill = {
description = "gokill daemon";
serviceConfig = {
Type = "simple";
ExecStart = "${gokill-pkg}/bin/gokill -c ${configFile}";
Restart = "on-failure";
};
docs = pkgs.callPackage (import ./docs/default.nix) { self = self; };
wantedBy = [ "default.target" ];
};
default = self.packages.${system}.gokill;
};
bundlers.gokillDeb = pkg: pkgs.stdenv.mkDerivation {
name = "deb-single-${pkg.name}";
buildInputs = [
pkgs.fpm
];
unpackPhase = "true";
buildPhase = ''
export HOME=$PWD
mkdir -p ./nix/store/
for item in "$(cat ${pkgs.referencesByPopularity pkg})"
do
cp -r $item ./nix/store/
done
mkdir -p ./bin
cp -r ${pkg}/bin/* ./bin/
mkdir -p ./etc
cp -r ${pkg}/etc/* ./etc/
chmod -R a+rwx ./nix
chmod -R a+rwx ./bin
chmod -R a+rwx ./etc
fpm -s dir -t deb --name ${pkg.name} nix bin etc
'';
installPhase = ''
mkdir -p $out
cp -r *.deb $out
'';
};
apps = {
docs = {
type = "app";
program = builtins.toString (pkgs.writeScript "docs" ''
${pkgs.python3}/bin/python3 -m http.server --directory ${self.packages."${system}".docs}/share/doc'');
};
exportDEB = {
type = "app";
program = builtins.toString (pkgs.writeScript "docs" ''
${pkgs.nix}/bin/nix bundle --bundler .#bundlers.${system}.gokillDeb .#packages.${system}.gokill'');
};
};
})) ({
nixosModules.gokill = import ./nixos-modules/gokill.nix { self = self; };
packages.x86_64-linux.testVm =
let
nixos = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
specialArgs = { inherit self; };
modules = [
self.nixosModules.gokill
{
services.gokill.enable = true;
services.gokill.testRun = false;
services.gokill.triggers = [
{
type = "Timeout";
@@ -139,19 +143,6 @@
duration = 10;
};
actions = [
{
type = "Timeout";
options = {
duration = 5;
};
stage = 1;
}
{
type = "Shutdown";
options = {
};
stage = 2;
}
];
}
];
@@ -159,7 +150,6 @@
virtualisation.vmVariant.virtualisation.graphics = false;
}
];
};
in
nixos.config.system.build.vm;
@@ -171,10 +161,13 @@
'');
};
apps.x86_64-linux.docs = {
type = "app";
program = builtins.toString (nixpkgs.legacyPackages."x86_64-linux".writeScript "docs" ''
${pkgs.python3}/bin/python3 -m http.server --directory ${self.packages."x86_64-linux".docs}/share/doc'');
checks.x86_64-linux = let
checkArgs = {
pkgs = nixpkgs.legacyPackages."x86_64-linux";
inherit self;
};
in {
gokill = import ./test/test.nix checkArgs;
};
};
}) ;
}

23
go.mod
View File

@@ -1,3 +1,26 @@
module unknown.com/gokill
go 1.21.3
require (
github.com/apsdehal/go-logger v0.0.0-20190515212710-b0d6ccfee0e6
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
github.com/mattn/go-sqlite3 v1.14.17
maunium.net/go/mautrix v0.16.1
)
require (
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/rs/zerolog v1.30.0 // indirect
github.com/tidwall/gjson v1.16.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
go.mau.fi/util v0.1.0 // indirect
golang.org/x/crypto v0.13.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.15.0 // indirect
golang.org/x/sys v0.12.0 // indirect
maunium.net/go/maulogger/v2 v2.4.1 // indirect
)

51
go.sum Normal file
View File

@@ -0,0 +1,51 @@
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/apsdehal/go-logger v0.0.0-20190515212710-b0d6ccfee0e6 h1:qISSdUEX4sjDHfdD/vf65fhuCh3pIhiILDB7ktjJrqU=
github.com/apsdehal/go-logger v0.0.0-20190515212710-b0d6ccfee0e6/go.mod h1:U3/8D6R9+bVpX0ORZjV+3mU9pQ86m7h1lESgJbXNvXA=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
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/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.16.0 h1:SyXa+dsSPpUlcwEDuKuEBJEz5vzTvOea+9rjyYodQFg=
github.com/tidwall/gjson v1.16.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
go.mau.fi/util v0.1.0 h1:BwIFWIOEeO7lsiI2eWKFkWTfc5yQmoe+0FYyOFVyaoE=
go.mau.fi/util v0.1.0/go.mod h1:AxuJUMCxpzgJ5eV9JbPWKRH8aAJJidxetNdUj7qcb84=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/maulogger/v2 v2.4.1 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8=
maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho=
maunium.net/go/mautrix v0.16.1 h1:Wb3CvOCe8A/NLsFeZYxKrgXKiqeZUQEBD1zqm7n/kWk=
maunium.net/go/mautrix v0.16.1/go.mod h1:2Jf15tulVtr6LxoiRL4smRXwpkGWUNfBFhwh/aXDBuk=

BIN
gokill Executable file

Binary file not shown.

63
internal/logging.go Normal file
View File

@@ -0,0 +1,63 @@
package internal
import (
"github.com/apsdehal/go-logger"
"fmt"
"os"
)
var Log *logger.Logger
var documenterLoggers map[string]*logger.Logger
var verbose bool
var formatNormal string = "%{message}"
var formatVerbose string = "%{time} %{level}: %{message}"
var formatModuleNormal string = "%{module}: %{message}"
var formatModuleVerbose string = "%{time} %{level} %{module}: %{message}"
func InitLogger() error {
var err error
Log, err = logger.New("UsbDisconnect", 1, os.Stdout)
if err != nil {
return fmt.Errorf("Logger initialization error: %s", err)
}
documenterLoggers = make(map[string]*logger.Logger)
verbose = false
Log.SetLogLevel(logger.InfoLevel)
return nil
}
func LogDoc(documenter Documenter) *logger.Logger {
documenterLogger, ok := documenterLoggers[documenter.GetName()]
if !ok {
documenterLogger, _ = logger.New(documenter.GetName(), 1, os.Stdout)
documenterLoggers[documenter.GetName()] = documenterLogger
}
SetVerbose(verbose)
//documenterLogger.SetFormat("%{message}")
return documenterLogger
}
func SetVerbose(value bool) {
verbose = value
toggleVerbosity := func(l *logger.Logger, formatN string, formatV string) {
if value {
l.SetLogLevel(logger.DebugLevel)
l.SetFormat(formatV)
} else {
l.SetLogLevel(logger.InfoLevel)
l.SetFormat(formatN)
}
}
toggleVerbosity(Log, formatNormal, formatVerbose)
for _, documenterLogger := range documenterLoggers {
toggleVerbosity(documenterLogger, formatModuleNormal, formatModuleVerbose)
}
}

82
nixos-modules/gokill.nix Normal file
View File

@@ -0,0 +1,82 @@
flake: { config, lib, pkgs, self, ... }:
let
cfg = config.services.gokill;
configFile = pkgs.writeText "config.json" (builtins.toJSON cfg.triggers);
gokill-pkg = self.packages.x86_64-linux.gokill;
testRun = if cfg.testRun then "-t" else "";
in
{
options = with lib; {
services.gokill = {
enable = mkOption {
default = false;
type = types.bool;
description = mdDoc ''
Enables gokill daemon
'';
};
testRun = mkOption {
default = false;
type = types.bool;
description = mdDoc ''
When set to true gokill is performing a test run
'';
};
triggers = mkOption {
description = "list of triggers";
default = [];
type = with types; types.listOf ( submodule {
options = {
type = mkOption {
type = types.str;
};
name = mkOption {
type = types.str;
};
options = mkOption {
type = types.attrs;
};
actions = mkOption {
description = "list of actions";
type = with types; types.listOf ( submodule {
options = {
type = mkOption {
type = types.str;
};
options = mkOption {
type = types.attrs;
};
stage = mkOption {
type = types.int;
};
};
});
};
};
});
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.gokill = {
description = "gokill daemon";
serviceConfig = {
Type = "simple";
ExecStart = "${gokill-pkg}/bin/gokill -c ${configFile} ${testRun}";
Restart = "on-failure";
};
wantedBy = [ "default.target" ];
};
};
}

2
test.sh Executable file
View File

@@ -0,0 +1,2 @@
#/bin/sh
echo "hello world"

21
test/lib.nix Normal file
View File

@@ -0,0 +1,21 @@
# tests/lib.nix
# based on https://blog.thalheim.io/2023/01/08/how-to-use-nixos-testing-framework-with-flakes/
# The first argument to this function is the test module itself
test:
# These arguments are provided by `flake.nix` on import, see checkArgs
{ pkgs, self}:
let
inherit (pkgs) lib;
# this imports the nixos library that contains our testing framework
nixos-lib = import (pkgs.path + "/nixos/lib") {};
in
(nixos-lib.runTest {
hostPkgs = pkgs;
# This speeds up the evaluation by skipping evaluating documentation (optional)
defaults.documentation.enable = lib.mkDefault false;
# This makes `self` available in the NixOS configuration of our virtual machines.
# This is useful for referencing modules or packages from your own flake
# as well as importing from other flakes.
node.specialArgs = { inherit self; };
imports = [ test ];
}).config.result

40
test/test.nix Normal file
View File

@@ -0,0 +1,40 @@
(import ./lib.nix) {
name = "gokill-base-test";
nodes = {
node1 = { self, pkgs, ... }: {
imports = [ self.nixosModules.gokill ];
services.gokill = {
enable = true;
triggers = [
{
type = "Timeout";
name = "custom timeout";
options = {
duration = 3;
};
actions = [
{
type = "Command";
options = {
command = "echo hello world";
};
stage = 2;
}
];
}
];
};
};
};
testScript = ''
import time
start_all() # wait for our service to start
node1.wait_for_unit("gokill")
time.sleep(4)
output = node1.succeed("journalctl -u gokill.service | tail -n 2 | head -n 1")
# Check if our webserver returns the expected result
assert "hello world" in output
'';
}

View File

@@ -20,7 +20,7 @@ func isEthernetConnected(deviceName string) bool {
content, err := os.ReadFile(fmt.Sprintf("/sys/class/net/%s/operstate", deviceName))
if err != nil {
fmt.Println(err)
internal.LogDoc(EthernetDisconnect{}).Errorf("Cant read devices operstate. Check the deviceName. error: %s", err)
return false
}

View File

@@ -0,0 +1,136 @@
package triggers
import (
"fmt"
"encoding/json"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"unknown.com/gokill/internal"
"unknown.com/gokill/actions"
)
type ReceiveTelegram struct {
Token string `json:"token"`
ChatId int64 `json:"chatId"`
Message string `json:"message"`
action actions.Action
}
func (s ReceiveTelegram) Listen() {
bot, err := tgbotapi.NewBotAPI(s.Token)
if err != nil {
return //fmt.Errorf("ReceiveTelegram waitForMessage error: %s", err)
}
bot.Debug = false
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
msg := tgbotapi.NewMessage(-746157642, "BOT TEST MESSAGE")
bot.Send(msg)
chatId := s.ChatId
updates := bot.GetUpdatesChan(u)
for update := range updates {
if update.Message != nil { // If we got a message
if(update.Message.Chat.ID != chatId) {
internal.LogDoc(s).Debugf("ReceiveTelegram received wrong ChatId. Got %s, wanted %s", update.Message.Chat.ID, s.ChatId)
continue
}
if(update.Message.Text != s.Message) {
internal.LogDoc(s).Debug("ReceiveTelegram received wrong Message")
continue
}
internal.LogDoc(s).Info("ReceiveTelegram received secret message")
actions.Fire(s.action)
}
}
}
func CreateReceiveTelegram(config internal.KillSwitchConfig) (ReceiveTelegram, error) {
result := ReceiveTelegram{
ChatId: 0,
}
err := json.Unmarshal(config.Options, &result)
if err != nil {
return ReceiveTelegram{}, fmt.Errorf("Error during CreateReceiveTelegram: %s", err)
}
if result.Token == "" {
return ReceiveTelegram{}, internal.OptionMissingError{"token"}
}
if result.ChatId == 0 {
return ReceiveTelegram{}, internal.OptionMissingError{"chadId"}
}
if result.Message == "" {
return ReceiveTelegram{}, internal.OptionMissingError{"message"}
}
action, err := actions.NewAction(config.Actions)
if err != nil {
return ReceiveTelegram{}, fmt.Errorf("Error during CreateReceiveTelegram: %s", err)
}
result.action = action
return result, nil
}
func (e ReceiveTelegram) Create(config internal.KillSwitchConfig) (Trigger, error) {
return CreateReceiveTelegram(config)
}
func (p ReceiveTelegram) GetName() string {
return "ReceiveTelegram"
}
func (p ReceiveTelegram) GetDescription() string {
return "Waits for a specific message in a given chat. Once the message is received, the trigger fires."
}
func (p ReceiveTelegram) GetExample() string {
return `
{
"type": "ReceiveTelegram",
"options": {
"token": "5349923487:FFGrETxa0pA29d02Akslw-lkwjdA92KAH2",
"chatId": -832345892,
"message": "secretmessagethatfiresthetrigger"
}
}
`
}
func (p ReceiveTelegram) GetOptions() []internal.ConfigOption {
return []internal.ConfigOption{
{
Name: "token",
Type: "string",
Description: "telegram bot token (ask botfather)",
Default: "",
},
{
Name: "chatId",
Type: "int",
Description: "chatId of group or chat you want the message be received from.",
Default: "",
},
{
Name: "message",
Type: "string",
Description: "actual message that, when received, fires the trigger",
Default: "",
},
}
}

View File

@@ -1,7 +1,6 @@
package triggers
import (
"encoding/json"
"fmt"
"time"
"unknown.com/gokill/actions"
@@ -14,10 +13,10 @@ type TimeOut struct {
}
func (t TimeOut) Listen() {
fmt.Println("TimeOut listens")
fmt.Println(t.Duration)
internal.LogDoc(t).Info("TimeOut listens")
internal.LogDoc(t).Infof("%d", t.Duration)
time.Sleep(time.Duration(t.Duration) * time.Second)
fmt.Println("TimeOut fires")
internal.LogDoc(t).Notice("TimeOut fires")
actions.Fire(t.action)
}

View File

@@ -28,8 +28,9 @@ func NewTrigger(config internal.KillSwitchConfig) (Trigger, error) {
func GetAllTriggers() []DocumentedTrigger {
return []DocumentedTrigger{
TimeOut{},
EthernetDisconnect{},
ReceiveTelegram{},
TimeOut{},
UsbDisconnect{},
}
}

View File

@@ -3,7 +3,6 @@ package triggers
import (
"encoding/json"
"errors"
"fmt"
"os"
"time"
@@ -35,8 +34,8 @@ func (t UsbDisconnect) Listen() {
time.Sleep(1 * time.Second)
}
fmt.Printf("Device %s detected.\n", t.DeviceName)
fmt.Println("UsbDisconnect Trigger is Armed")
internal.LogDoc(t).Infof("Device %s detected.", t.DeviceName)
internal.LogDoc(t).Notice("Trigger is Armed")
}
for {