Skip to content

Commit

Permalink
Initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
juan-leon committed Jul 3, 2021
1 parent a03cd64 commit 3b8194e
Show file tree
Hide file tree
Showing 30 changed files with 1,721 additions and 10 deletions.
20 changes: 20 additions & 0 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Lint

on:
push:
tags:
- v*
branches:
- main
pull_request:

jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
version: v1.41.1
26 changes: 26 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Release
on:
push:
# The idea here is to trigger a release upon receiving a release-like tag
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'

jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v2
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: 1.16
- name: Unshallow
run: git fetch --prune --unshallow
- name: Create release
uses: goreleaser/goreleaser-action@v2
with:
version: latest
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
13 changes: 13 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: Test

on: [push, pull_request]

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout source code
uses: actions/checkout@v2
- name: Run tests
run: make test
12 changes: 2 additions & 10 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,10 +1,2 @@
# Generated by Cargo
# will have compiled files and executables
/target/

# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock

# These are backup files generated by rustfmt
**/*.rs.bk
bin/*
.coverage.out
57 changes: 57 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
linters:
enable:
- bodyclose
- deadcode
- depguard
- dogsled
- dupl
- funlen
- gochecknoinits
- goconst
- gocritic
- gocyclo
- gofmt
- goimports
- revive
- goprintffuncname
- gosec
- gosimple
- govet
- ineffassign
- misspell
- nakedret
- rowserrcheck
- exportloopref
- staticcheck
- structcheck
- stylecheck
- typecheck
- unconvert
- unparam
- unused
- varcheck
- whitespace
disable:
# Some errors are just logged when found, but not checked
- errcheck
issues:
exclude-rules:
- linters:
- revive
text: "don't use ALL_CAPS in Go names"
- linters:
- stylecheck
text: "ST1003: should not use ALL_CAPS in Go names"
# Exclude some linters from running on tests files.
- path: _test\.go
linters:
- gocyclo
- dupl
- gosec
- funlen
linters-settings:
funlen:
lines: 100
statements: 40
misspell:
locale: US
3 changes: 3 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
builds:
- goos:
- linux
87 changes: 87 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# The go toolchain version we want to use
GOVERSION = 1.16.5

# Where we will install a modern go if none is available (note that this
# variable can be overriden in the environment, if needed)
INSTALL_PATH ?= $(HOME)/goroot

# Go commands
GOCMD = go
GOFMT = gofmt -l
GOBUILD = $(GOCMD) build
GOTEST = $(GOCMD) test
GOLINT = golangci-lint run

PROJECT = github.com/juan-leon/fetter
GOBIN = bin
EXEC = bin/fetter

export PATH := $(INSTALL_PATH)/go/bin:$(HOME)/bin:$(PATH)

# PATH is not inherited by shells spawned by "shell" function
go_version := $(shell PATH=$(PATH) go version 2>/dev/null)
linter_version := $(shell PATH=$(PATH) golangci-lint --version 2>/dev/null)
now := $(shell date +'%Y-%m-%dT%T')
src := $(shell find -name '*.go')
sha := $(shell git log -1 --pretty=%H 2>/dev/null || echo unknown)

# Version can be overwritten via env var. If not present, we figure it out from
# git. The "word 1" is an ultra paranoid protection against spaces in tag name:
# those are not liked by the linker unless escaped.
version ?= $(word 1, $(shell git describe --abbrev --tags 2>/dev/null || echo unknown))

define install_go
@echo Installing Go $(GOVERSION)
mkdir -p $(INSTALL_PATH)
curl -s https://storage.googleapis.com/golang/go$(GOVERSION).linux-amd64.tar.gz | tar -C $(INSTALL_PATH) -xz
@echo Done installing Go $(GOVERSION)
endef

define install_linter
@echo Installing linter
mkdir -p $(HOME)/bin
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(HOME)/bin v1.41.1
@echo Done installing linter
endef

.PHONY: clean test toolchain linter lint


build: $(EXEC)

$(EXEC): $(src)
$(GOBUILD) \
--ldflags "-X main.Commit=$(sha) -X main.BuildDate=$(now) -X main.Version=$(version)" \
-o $(EXEC) \
github.com/juan-leon/fetter

clean:
rm -f $(EXEC)

# Format source code files
fmt: toolchain
$(GOFMT) -w .

# Prints the source code files poorly formatted
lint:
@echo Linting code
$(GOLINT)

# Run tests
test:
$(GOTEST) -coverprofile=.coverage.out $(PROJECT)/...
@echo Code coverage
@go tool cover -func=.coverage.out | tail -n 1
@echo "Use 'go tool cover -html=.coverage.out' to inspect results"

# Make sure we have go installed, or install it otherwise
toolchain:
ifeq (, $(findstring $(GOVERSION), $(go_version)))
$(call install_go)
endif

# Make sure we have go 1.24 installed, or install it otherwise
linter: toolchain
ifeq (, $(findstring 1.41, $(linter_version)))
$(call install_linter)
endif
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,14 @@
# fetter

Move processes into control groups based on configurable actions

[![Test status](https://github.com/juan-leon/fetter/actions/workflows/test.yml/badge.svg)](https://github.com/juan-leon/fetter/actions/fetter/test.yml)
[![Lint status](https://github.com/juan-leon/fetter/actions/workflows/lint.yaml/badge.svg)](https://github.com/juan-leon/fetter/actions/fetter/lint.yaml)
[![Release](https://img.shields.io/github/release/juan-leon/fetter.svg)](https://github.com/juan-leon/fetter/releases/latest)

## TODO

Write documentation.

In the meanwhile, this example of configuration will give you a hint of what the
tool can be used for: [![Sample configuration file](examples/documented-example.yaml)]
5 changes: 5 additions & 0 deletions TODO.org
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
* TODO tests
* TODO unit tests
* TODO docs
* TODO badges
* TODO CI pipeline (release, coverage)
151 changes: 151 additions & 0 deletions examples/documented-example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
---
# Two basic modes are supported: audit and scanner.
#
# Audit mode is recommended: it sets audit rules to the kernel and keep a
# netlink connection open so that as soon as a rule is matched the process can
# be moved to a control group. This mode supports detection of running
# applications by path, writing or reading specific files (or directories).
# This mode consumes few resources, since program is just listening.
#
# Scanner mode only works for 'execute' actions. The running processes will be
# scanned every second, and matches, if any, will be distributed on groups.
# Scanning is more expensive than listening to a netlink socket. This mode is
# recommended for those scenarios where audit rules are locked by administrator
# (once locked, they cannot be unlocked without rebooting the machine), or the
# Linux kernel is ancient and does not support multicast for Netlink
#
# Default is audit
mode: audit
audit:
# There are three audit modes (meaningless in scanner mode) that dictates how
# to setup the audit rules in the kernel: override, preserve and reuse
#
# * When override is used, program will delete any existing rules and leave
# only the ones configured
#
# * When preserve is used, program will add its rules over whatever rule
# already configured. This is useful for coexisting with auditd. However,
# if a lot of rule rewriting rules is done, old rules are not removed. and
# that can lead to surprises.
#
# * When reuse is used, no rules will be set up. The use case if for those
# scenarios where you want to configure the rules in separate runs of this
# program (on run to configure rules, other to run as daemon)
mode: override

logging:
# File name where logs will be written
file: /tmp/fetter.log
# Standard error levels available. Debug shows interesting info and it is not
# too verbose.
level: info

# This is the name of the cgroup path used by application (all cgroups created
# by this program will belong to it). Default is 'fetter'; there is no reason
# to change it other than doing experiments or using several fetter applications
# in parallel.
name: fetter

# These are the rules. By default there is none; the ones below are just
# examples.
rules:
# Following rule will use a cgroup named browser for firefox. Notice that you
# need to know the name of the firefox executable (if in doubt, you can figure
# it out by doing `ls -l /proc/PID/exe` to know the path, and 'ps -u | grep
# firefox' to know the PID)
- path: /usr/lib/firefox/firefox
# Supported actions are execute (the most useful one: the process executing
# a file will be moved to a control group), read, and write.
action: execute
# Name of the group should match one of the groups defined in their section.
group: browsers

# You can make several applications to share same cgroup, if you want. That
# way, the limits for that cgroup apply to both at once
- path: /usr/lib/chromium-browser/chromium-browser
action: execute
group: browsers

- path: /usr/bin/emacs
action: execute
group: ides

- path: /my/forbidden/file
action: write
# KILL is not a real cgroup, but a way to say fetter: kill whatever process
# doing that action. In this case, whenever a process writes to the file in
# path, process will be killed
group: KILL

# This is an example where a process reading a file will be frozen in place by
# the operating system (group honeypot has "freeze: true"). Process execution
# will not continue, and it cannot be killed unless removed from cgroup, or
# cgroup is manually thawed. This will allow you to detect what processes
# read/write to a file and examine them.
- path: /my/forbidden/file
action: read
group: honeypot


# These control groups will be created by the application, with the limits
# specified for any of them. By default there is none; the ones below are just
# examples.
#
# Note that while it is safe to cap CPU to any application, capping pids and or
# RAM might make those applications malfunction. That would depend on how the
# applications manage error codes of operations that are denied by operating
# system. Those operations would be the ones related to asking more RAM we
# allow them to use, or trying to spawn more children. Think of a browser that
# uses a process-per-tab approach: if we cap processes to 20, the tab 21st would
# fail to display correctly
groups:
- name: browsers
# Max RAM, in Mbs, that all the processes in the group together can use.
ram: 2000
# Max number of processes that can be spawned simultaneously by processes in
# the group. A process spawned by a process of a group will remain in the
# group.
pids: 30
# Max single-CPU %-age that processes in the group will be able to use. For
# instance, if you want to make sure your massively heavy parallel local
# compilations do not make your UI unusable, you can create a group for
# 'make' with a CPU limit. Note that if you have N CPUs, you might want to
# use values higher than 100. For instance, in a machine with 8 CPUs
# meaningful values are those between 0 (no juice) and 800 (no limit). A
# value of 400 would mean half of the CPU power would be available for other
# tasks.
cpu: 250
# Default is false. true means that the group is a freezer: processes
# cannot continue execution or be killed by their owners (unless they are
# root and familiar with the freeze subsystem). Use of this feature is to
# allow to detect and examine processes that do some action. Use with
# caution.
freeze: false
# Default is false. true means that instead of moving the process to a
# cgroup the process will be killed. It is a way of making sure (or
# enforcing) some actions are never done. Use with caution.
kill: false

- name: ides
ram: 1000
pids: 50
cpu: 80

- name: email
ram: 2000
pids: 5
cpu: 50

- name: music
ram: 500
pids: 5
cpu: 80

# This example has unlimited ram, as it is not specified
- name: shell
cpu: 95
pids: 100

# Example of a group to freeze processes
- name: honeypot
freeze: true
Loading

0 comments on commit 3b8194e

Please sign in to comment.