mirror of
https://github.com/evilsocket/opensnitch.git
synced 2025-03-04 08:34:40 +01:00
400 lines
14 KiB
Go
400 lines
14 KiB
Go
package ui
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/evilsocket/opensnitch/daemon/core"
|
|
"github.com/evilsocket/opensnitch/daemon/firewall"
|
|
"github.com/evilsocket/opensnitch/daemon/log"
|
|
"github.com/evilsocket/opensnitch/daemon/procmon/monitor"
|
|
"github.com/evilsocket/opensnitch/daemon/rule"
|
|
"github.com/evilsocket/opensnitch/daemon/tasks"
|
|
"github.com/evilsocket/opensnitch/daemon/tasks/nodemonitor"
|
|
"github.com/evilsocket/opensnitch/daemon/tasks/pidmonitor"
|
|
"github.com/evilsocket/opensnitch/daemon/tasks/socketsmonitor"
|
|
"github.com/evilsocket/opensnitch/daemon/ui/config"
|
|
"github.com/evilsocket/opensnitch/daemon/ui/protocol"
|
|
"golang.org/x/net/context"
|
|
)
|
|
|
|
// NewReply constructs a new protocol notification reply
|
|
func NewReply(rID uint64, replyCode protocol.NotificationReplyCode, data string) *protocol.NotificationReply {
|
|
return &protocol.NotificationReply{
|
|
Id: rID,
|
|
Code: replyCode,
|
|
Data: data,
|
|
}
|
|
}
|
|
|
|
func (c *Client) getClientConfig() *protocol.ClientConfig {
|
|
raw, _ := ioutil.ReadFile(configFile)
|
|
nodeName := core.GetHostname()
|
|
nodeVersion := core.GetKernelVersion()
|
|
var ts time.Time
|
|
rulesTotal := len(c.rules.GetAll())
|
|
ruleList := make([]*protocol.Rule, rulesTotal)
|
|
idx := 0
|
|
for _, r := range c.rules.GetAll() {
|
|
ruleList[idx] = r.Serialize()
|
|
idx++
|
|
}
|
|
sysfw, err := firewall.Serialize()
|
|
if err != nil {
|
|
log.Warning("firewall.Serialize() error: %s", err)
|
|
}
|
|
return &protocol.ClientConfig{
|
|
Id: uint64(ts.UnixNano()),
|
|
Name: nodeName,
|
|
Version: nodeVersion,
|
|
IsFirewallRunning: firewall.IsRunning(),
|
|
Config: strings.Replace(string(raw), "\n", "", -1),
|
|
LogLevel: uint32(log.MinLevel),
|
|
Rules: ruleList,
|
|
SystemFirewall: sysfw,
|
|
}
|
|
}
|
|
|
|
func (c *Client) handleActionChangeConfig(stream protocol.UI_NotificationsClient, notification *protocol.Notification) {
|
|
log.Info("[notification] Reloading configuration")
|
|
// Parse received configuration first, to get the new proc monitor method.
|
|
newConf, err := config.Parse(notification.Data)
|
|
if err != nil {
|
|
log.Warning("[notification] error parsing received config: %v", notification.Data)
|
|
c.sendNotificationReply(stream, notification.Id, "", err)
|
|
return
|
|
}
|
|
|
|
if err := c.reloadConfiguration(true, newConf); err != nil {
|
|
c.sendNotificationReply(stream, notification.Id, "", err.Msg)
|
|
return
|
|
}
|
|
|
|
// this save operation triggers a regular re-loadConfiguration()
|
|
err = config.Save(configFile, notification.Data)
|
|
if err != nil {
|
|
log.Warning("[notification] CHANGE_CONFIG not applied %s", err)
|
|
}
|
|
|
|
c.sendNotificationReply(stream, notification.Id, "", err)
|
|
}
|
|
|
|
func (c *Client) handleActionEnableRule(stream protocol.UI_NotificationsClient, notification *protocol.Notification) {
|
|
var err error
|
|
for _, rul := range notification.Rules {
|
|
log.Info("[notification] enable rule: %s", rul.Name)
|
|
// protocol.Rule(protobuf) != rule.Rule(json)
|
|
r, _ := rule.Deserialize(rul)
|
|
r.Enabled = true
|
|
// save to disk only if the duration is rule.Always
|
|
err = c.rules.Replace(r, r.Duration == rule.Always)
|
|
}
|
|
c.sendNotificationReply(stream, notification.Id, "", err)
|
|
}
|
|
|
|
func (c *Client) handleActionDisableRule(stream protocol.UI_NotificationsClient, notification *protocol.Notification) {
|
|
var err error
|
|
for _, rul := range notification.Rules {
|
|
log.Info("[notification] disable rule: %s", rul)
|
|
r, _ := rule.Deserialize(rul)
|
|
r.Enabled = false
|
|
err = c.rules.Replace(r, r.Duration == rule.Always)
|
|
}
|
|
c.sendNotificationReply(stream, notification.Id, "", err)
|
|
}
|
|
|
|
func (c *Client) handleActionChangeRule(stream protocol.UI_NotificationsClient, notification *protocol.Notification) {
|
|
var rErr error
|
|
for _, rul := range notification.Rules {
|
|
r, err := rule.Deserialize(rul)
|
|
if r == nil {
|
|
rErr = fmt.Errorf("Invalid rule, %s", err)
|
|
continue
|
|
}
|
|
log.Info("[notification] change rule: %s %d", r, notification.Id)
|
|
if err := c.rules.Replace(r, r.Duration == rule.Always); err != nil {
|
|
log.Warning("[notification] Error changing rule: %s %s", err, r)
|
|
rErr = err
|
|
}
|
|
}
|
|
c.sendNotificationReply(stream, notification.Id, "", rErr)
|
|
}
|
|
|
|
func (c *Client) handleActionDeleteRule(stream protocol.UI_NotificationsClient, notification *protocol.Notification) {
|
|
var err error
|
|
for _, rul := range notification.Rules {
|
|
log.Info("[notification] delete rule: %s %d", rul.Name, notification.Id)
|
|
err = c.rules.Delete(rul.Name)
|
|
if err != nil {
|
|
log.Error("[notification] Error deleting rule: %s %s", err, rul)
|
|
}
|
|
}
|
|
c.sendNotificationReply(stream, notification.Id, "", err)
|
|
}
|
|
|
|
func (c *Client) handleActionTaskStart(stream protocol.UI_NotificationsClient, notification *protocol.Notification) {
|
|
var taskConf tasks.TaskNotification
|
|
err := json.Unmarshal([]byte(notification.Data), &taskConf)
|
|
if err != nil {
|
|
log.Error("parsing TaskStart, err: %s, %s", err, notification.Data)
|
|
c.sendNotificationReply(stream, notification.Id, "", err)
|
|
return
|
|
}
|
|
switch taskConf.Name {
|
|
case pidmonitor.Name:
|
|
conf, ok := taskConf.Data.(map[string]interface{})
|
|
if !ok {
|
|
log.Error("[pidmon] TaskStart.Data, PID err (string expected): %v", taskConf)
|
|
return
|
|
}
|
|
pid, err := strconv.Atoi(conf["pid"].(string))
|
|
if err != nil {
|
|
log.Error("[pidmon] TaskStart.Data, PID err: %s, %v", err, taskConf)
|
|
c.sendNotificationReply(stream, notification.Id, "", err)
|
|
return
|
|
}
|
|
interval, _ := conf["interval"].(string)
|
|
c.monitorProcessDetails(pid, interval, stream, notification)
|
|
case nodemonitor.Name:
|
|
conf, ok := taskConf.Data.(map[string]interface{})
|
|
if !ok {
|
|
log.Error("[nodemon] TaskStart.Data, \"node\" err (string expected): %v", taskConf)
|
|
return
|
|
}
|
|
c.monitorNode(conf["node"].(string), conf["interval"].(string), stream, notification)
|
|
case socketsmonitor.Name:
|
|
c.monitorSockets(taskConf.Data, stream, notification)
|
|
default:
|
|
log.Debug("TaskStart, unknown task: %v", taskConf)
|
|
//c.sendNotificationReply(stream, notification.Id, "", err)
|
|
}
|
|
}
|
|
|
|
func (c *Client) handleActionTaskStop(stream protocol.UI_NotificationsClient, notification *protocol.Notification) {
|
|
var taskConf tasks.TaskNotification
|
|
err := json.Unmarshal([]byte(notification.Data), &taskConf)
|
|
if err != nil {
|
|
log.Error("parsing TaskStop, err: %s, %s", err, notification.Data)
|
|
c.sendNotificationReply(stream, notification.Id, "", fmt.Errorf("Error stopping task: %s", notification.Data))
|
|
return
|
|
}
|
|
switch taskConf.Name {
|
|
case pidmonitor.Name:
|
|
conf, ok := taskConf.Data.(map[string]interface{})
|
|
if !ok {
|
|
log.Error("[pidmon] TaskStop.Data, PID err (string expected): %v", taskConf)
|
|
return
|
|
}
|
|
pid, err := strconv.Atoi(conf["pid"].(string))
|
|
if err != nil {
|
|
log.Error("TaskStop.Data, err: %s, %s, %v+, %q", err, notification.Data, taskConf.Data, taskConf.Data)
|
|
c.sendNotificationReply(stream, notification.Id, "", err)
|
|
return
|
|
}
|
|
TaskMgr.RemoveTask(fmt.Sprint(taskConf.Name, "-", pid))
|
|
case nodemonitor.Name:
|
|
conf, ok := taskConf.Data.(map[string]interface{})
|
|
if !ok {
|
|
log.Error("[pidmon] TaskStop.Data, PID err (string expected): %v", taskConf)
|
|
return
|
|
}
|
|
TaskMgr.RemoveTask(fmt.Sprint(nodemonitor.Name, "-", conf["node"].(string)))
|
|
case socketsmonitor.Name:
|
|
TaskMgr.RemoveTask(socketsmonitor.Name)
|
|
default:
|
|
log.Debug("TaskStop, unknown task: %v", taskConf)
|
|
//c.sendNotificationReply(stream, notification.Id, "", err)
|
|
}
|
|
}
|
|
|
|
func (c *Client) handleActionEnableInterception(stream protocol.UI_NotificationsClient, notification *protocol.Notification) {
|
|
log.Info("[notification] starting interception")
|
|
if err := monitor.ReconfigureMonitorMethod(c.config.ProcMonitorMethod, c.config.Ebpf); err != nil && err.What > monitor.NoError {
|
|
log.Warning("[notification] error enabling monitor (%s): %s", c.config.ProcMonitorMethod, err.Msg)
|
|
c.sendNotificationReply(stream, notification.Id, "", err.Msg)
|
|
return
|
|
}
|
|
if err := firewall.EnableInterception(); err != nil {
|
|
log.Warning("[notification] firewall.EnableInterception() error: %s", err)
|
|
c.sendNotificationReply(stream, notification.Id, "", err)
|
|
return
|
|
}
|
|
c.sendNotificationReply(stream, notification.Id, "", nil)
|
|
}
|
|
|
|
func (c *Client) handleActionDisableInterception(stream protocol.UI_NotificationsClient, notification *protocol.Notification) {
|
|
log.Info("[notification] stopping interception")
|
|
monitor.End()
|
|
if err := firewall.DisableInterception(); err != nil {
|
|
log.Warning("firewall.DisableInterception() error: %s", err)
|
|
c.sendNotificationReply(stream, notification.Id, "", err)
|
|
return
|
|
}
|
|
c.sendNotificationReply(stream, notification.Id, "", nil)
|
|
}
|
|
|
|
func (c *Client) handleActionReloadFw(stream protocol.UI_NotificationsClient, notification *protocol.Notification) {
|
|
log.Info("[notification] reloading firewall")
|
|
|
|
sysfw, err := firewall.Deserialize(notification.SysFirewall)
|
|
if err != nil {
|
|
log.Warning("firewall.Deserialize() error: %s", err)
|
|
c.sendNotificationReply(stream, notification.Id, "", fmt.Errorf("Error reloading firewall, invalid rules"))
|
|
return
|
|
}
|
|
if err := firewall.SaveConfiguration(sysfw); err != nil {
|
|
c.sendNotificationReply(stream, notification.Id, "", fmt.Errorf("Error saving system firewall rules: %s", err))
|
|
return
|
|
}
|
|
// TODO:
|
|
// - add new API endpoints to delete, add or change rules atomically.
|
|
// - a global goroutine where errors can be sent to the server (GUI).
|
|
go func(c *Client) {
|
|
var errors string
|
|
for {
|
|
select {
|
|
case fwerr := <-firewall.ErrorsChan():
|
|
errors = fmt.Sprint(errors, fwerr, ",")
|
|
if firewall.ErrChanEmpty() {
|
|
goto ExitWithError
|
|
}
|
|
|
|
// FIXME: can this operation last longer than 2s? if there're more than.. 100...10000 rules?
|
|
case <-time.After(2 * time.Second):
|
|
log.Debug("[notification] reload firewall. timeout fired, no errors?")
|
|
c.sendNotificationReply(stream, notification.Id, "", nil)
|
|
goto Exit
|
|
|
|
}
|
|
}
|
|
ExitWithError:
|
|
c.sendNotificationReply(stream, notification.Id, "", fmt.Errorf("%s", errors))
|
|
Exit:
|
|
}(c)
|
|
|
|
}
|
|
|
|
func (c *Client) handleNotification(stream protocol.UI_NotificationsClient, notification *protocol.Notification) {
|
|
switch {
|
|
case notification.Type == protocol.Action_TASK_START:
|
|
c.handleActionTaskStart(stream, notification)
|
|
|
|
case notification.Type == protocol.Action_TASK_STOP:
|
|
c.handleActionTaskStop(stream, notification)
|
|
|
|
case notification.Type == protocol.Action_CHANGE_CONFIG:
|
|
c.handleActionChangeConfig(stream, notification)
|
|
|
|
case notification.Type == protocol.Action_ENABLE_INTERCEPTION:
|
|
c.handleActionEnableInterception(stream, notification)
|
|
|
|
case notification.Type == protocol.Action_DISABLE_INTERCEPTION:
|
|
c.handleActionDisableInterception(stream, notification)
|
|
|
|
case notification.Type == protocol.Action_RELOAD_FW_RULES:
|
|
c.handleActionReloadFw(stream, notification)
|
|
|
|
// ENABLE_RULE just replaces the rule on disk
|
|
case notification.Type == protocol.Action_ENABLE_RULE:
|
|
c.handleActionEnableRule(stream, notification)
|
|
|
|
case notification.Type == protocol.Action_DISABLE_RULE:
|
|
c.handleActionDisableRule(stream, notification)
|
|
|
|
case notification.Type == protocol.Action_DELETE_RULE:
|
|
c.handleActionDeleteRule(stream, notification)
|
|
|
|
// CHANGE_RULE can add() or replace() an existing rule.
|
|
case notification.Type == protocol.Action_CHANGE_RULE:
|
|
c.handleActionChangeRule(stream, notification)
|
|
}
|
|
}
|
|
|
|
func (c *Client) sendNotificationReply(stream protocol.UI_NotificationsClient, nID uint64, data string, err error) error {
|
|
reply := NewReply(nID, protocol.NotificationReplyCode_OK, data)
|
|
if err != nil {
|
|
reply.Code = protocol.NotificationReplyCode_ERROR
|
|
reply.Data = fmt.Sprint(err)
|
|
}
|
|
if err := stream.Send(reply); err != nil {
|
|
log.Error("Error replying to notification: %s %d", err, reply.Id)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Subscribe opens a connection with the server (UI), to start
|
|
// receiving notifications.
|
|
// It firstly sends the daemon status and configuration.
|
|
func (c *Client) Subscribe() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
|
defer cancel()
|
|
|
|
clientCfg, err := c.client.Subscribe(ctx, c.getClientConfig())
|
|
if err != nil {
|
|
log.Error("Subscribing to GUI %s", err)
|
|
// When connecting to the GUI via TCP, sometimes the notifications channel is
|
|
// not established, and the main channel is never closed.
|
|
// We need to disconnect everything after a timeout and try it again.
|
|
c.disconnect()
|
|
return
|
|
}
|
|
|
|
if tempConf, err := config.Parse(clientCfg.Config); err == nil {
|
|
c.Lock()
|
|
clientConnectedRule.Action = rule.Action(tempConf.DefaultAction)
|
|
c.Unlock()
|
|
}
|
|
c.listenForNotifications()
|
|
}
|
|
|
|
// Notifications is the channel where the daemon receives messages from the server.
|
|
// It consists of 2 grpc streams (send/receive) that are never closed,
|
|
// this way we can share messages in realtime.
|
|
// If the GUI is closed, we'll receive an error reading from the channel.
|
|
func (c *Client) listenForNotifications() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
// open the stream channel
|
|
streamReply := &protocol.NotificationReply{Id: 0, Code: protocol.NotificationReplyCode_OK}
|
|
notisStream, err := c.client.Notifications(ctx)
|
|
if err != nil {
|
|
log.Error("establishing notifications channel %s", err)
|
|
return
|
|
}
|
|
// send the first notification
|
|
if err := notisStream.Send(streamReply); err != nil {
|
|
log.Error("sending notification HELLO %s", err)
|
|
return
|
|
}
|
|
log.Info("Start receiving notifications")
|
|
for {
|
|
select {
|
|
case <-c.clientCtx.Done():
|
|
goto Exit
|
|
default:
|
|
noti, err := notisStream.Recv()
|
|
if err == io.EOF {
|
|
log.Warning("notification channel closed by the server")
|
|
goto Exit
|
|
}
|
|
if err != nil {
|
|
log.Error("getting notifications: %s %s", err, noti)
|
|
goto Exit
|
|
}
|
|
c.handleNotification(notisStream, noti)
|
|
}
|
|
}
|
|
Exit:
|
|
notisStream.CloseSend()
|
|
log.Info("Stop receiving notifications")
|
|
c.disconnect()
|
|
TaskMgr.StopAll()
|
|
}
|