diff --git a/daemon/ui/auth/auth.go b/daemon/ui/auth/auth.go new file mode 100644 index 00000000..b02a7e8d --- /dev/null +++ b/daemon/ui/auth/auth.go @@ -0,0 +1,102 @@ +package auth + +import ( + "crypto/tls" + "crypto/x509" + "io/ioutil" + + "github.com/evilsocket/opensnitch/daemon/log" + "github.com/evilsocket/opensnitch/daemon/ui/config" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +// client auth types: +// https://pkg.go.dev/crypto/tls#ClientAuthType +var ( + clientAuthType = map[string]tls.ClientAuthType{ + "no-client-cert": tls.NoClientCert, + "req-cert": tls.RequestClientCert, + "req-any-cert": tls.RequireAnyClientCert, + "verify-cert": tls.VerifyClientCertIfGiven, + "req-and-verify-cert": tls.RequireAndVerifyClientCert, + } +) + +const ( + // AuthSimple will use WithInsecure() + AuthSimple = "simple" + + // AuthTLSSimple will use a common CA certificate, shared between the server + // and all the clients. + AuthTLSSimple = "tls-simple" + + // AuthTLSMutual will use a CA certificate and a client cert and key + // to authenticate each client. + AuthTLSMutual = "tls-mutual" +) + +// New returns the configuration that the UI will use +// to connect with the server. +func New(config *config.Config) (grpc.DialOption, error) { + config.RLock() + + credsType := config.Server.Authentication.Type + tlsOpts := config.Server.Authentication.TLSOptions + + config.RUnlock() + + if credsType == "" || credsType == AuthSimple { + log.Debug("UI auth: simple") + return grpc.WithInsecure(), nil + } + certPool := x509.NewCertPool() + + // use CA certificate to authenticate clients if supplied + if tlsOpts.CACert != "" { + if caPem, err := ioutil.ReadFile(tlsOpts.CACert); err != nil { + log.Warning("reading UI auth CA certificate (%s): %s", credsType, err) + } else { + if !certPool.AppendCertsFromPEM(caPem) { + log.Warning("adding UI auth CA certificate (%s): %s", credsType, err) + } + } + } + + // use server certificate to authenticate clients if supplied + if tlsOpts.ServerCert != "" { + if serverPem, err := ioutil.ReadFile(tlsOpts.ServerCert); err != nil { + log.Warning("reading auth server cert: %s", err) + } else { + if !certPool.AppendCertsFromPEM(serverPem) { + log.Warning("adding UI auth server cert (%s): %s", credsType, err) + } + } + } + + // set config of tls credential + // https://pkg.go.dev/crypto/tls#Config + tlsCfg := &tls.Config{ + InsecureSkipVerify: tlsOpts.SkipVerify, + RootCAs: certPool, + } + + // https://pkg.go.dev/google.golang.org/grpc/credentials#SecurityLevel + if credsType == AuthTLSMutual { + tlsCfg.ClientAuth = clientAuthType[tlsOpts.ClientAuthType] + clientCert, err := tls.LoadX509KeyPair( + tlsOpts.ClientCert, + tlsOpts.ClientKey, + ) + if err != nil { + return nil, err + } + log.Debug(" using client cert: %s", tlsOpts.ClientCert) + log.Debug(" using client key: %s", tlsOpts.ClientKey) + tlsCfg.Certificates = []tls.Certificate{clientCert} + } + + return grpc.WithTransportCredentials( + credentials.NewTLS(tlsCfg), + ), nil +} diff --git a/daemon/ui/client.go b/daemon/ui/client.go index 926cf4e1..32d4aafb 100644 --- a/daemon/ui/client.go +++ b/daemon/ui/client.go @@ -12,6 +12,8 @@ import ( "github.com/evilsocket/opensnitch/daemon/log/loggers" "github.com/evilsocket/opensnitch/daemon/rule" "github.com/evilsocket/opensnitch/daemon/statistics" + "github.com/evilsocket/opensnitch/daemon/ui/auth" + "github.com/evilsocket/opensnitch/daemon/ui/config" "github.com/evilsocket/opensnitch/daemon/ui/protocol" "github.com/fsnotify/fsnotify" @@ -28,32 +30,11 @@ var ( // While the GUI is connected, deny by default everything until the user takes an action. clientConnectedRule = rule.Create("ui.client.connected", "", true, false, false, rule.Deny, rule.Once, dummyOperator) clientErrorRule = rule.Create("ui.client.error", "", true, false, false, rule.Allow, rule.Once, dummyOperator) - config Config + clientConfig config.Config maxQueuedAlerts = 1024 ) -type serverConfig struct { - Address string `json:"Address"` - LogFile string `json:"LogFile"` - Loggers []loggers.LoggerConfig `json:"Loggers"` -} - -// Config holds the values loaded from configFile -type Config struct { - sync.RWMutex - Server serverConfig `json:"Server"` - DefaultAction string `json:"DefaultAction"` - DefaultDuration string `json:"DefaultDuration"` - InterceptUnknown bool `json:"InterceptUnknown"` - ProcMonitorMethod string `json:"ProcMonitorMethod"` - LogLevel *uint32 `json:"LogLevel"` - LogUTC bool `json:"LogUTC"` - LogMicro bool `json:"LogMicro"` - Firewall string `json:"Firewall"` - Stats statistics.StatsConfig `json:"Stats"` -} - // Client holds the connection information of a client. type Client struct { sync.RWMutex @@ -98,8 +79,8 @@ func NewClient(socketPath string, stats *statistics.Statistics, rules *rule.Load if socketPath != "" { c.setSocketPath(c.getSocketPath(socketPath)) } - loggers.Load(config.Server.Loggers, config.Stats.Workers) - stats.SetLimits(config.Stats) + loggers.Load(clientConfig.Server.Loggers, clientConfig.Stats.Workers) + stats.SetLimits(clientConfig.Stats) stats.SetLoggers(loggers) return c @@ -118,26 +99,26 @@ func (c *Client) Close() { // ProcMonitorMethod returns the monitor method configured. // If it's not present in the config file, it'll return an empty string. func (c *Client) ProcMonitorMethod() string { - config.RLock() - defer config.RUnlock() - return config.ProcMonitorMethod + clientConfig.RLock() + defer clientConfig.RUnlock() + return clientConfig.ProcMonitorMethod } // InterceptUnknown returns func (c *Client) InterceptUnknown() bool { - config.RLock() - defer config.RUnlock() - return config.InterceptUnknown + clientConfig.RLock() + defer clientConfig.RUnlock() + return clientConfig.InterceptUnknown } // GetFirewallType returns the firewall to use func (c *Client) GetFirewallType() string { - config.RLock() - defer config.RUnlock() - if config.Firewall == "" { + clientConfig.RLock() + defer clientConfig.RUnlock() + if clientConfig.Firewall == "" { return iptables.Name } - return config.Firewall + return clientConfig.Firewall } // DefaultAction returns the default configured action for @@ -280,7 +261,11 @@ func (c *Client) openSocket() (err error) { PermitWithoutStream: true, } - c.con, err = grpc.Dial(c.socketPath, grpc.WithInsecure(), grpc.WithKeepaliveParams(kacp)) + dialOption, err := auth.New(&clientConfig) + if err != nil { + return fmt.Errorf("Invalid client auth options: %s", err) + } + c.con, err = grpc.Dial(c.socketPath, dialOption, grpc.WithKeepaliveParams(kacp)) } return err diff --git a/daemon/ui/config/config.go b/daemon/ui/config/config.go new file mode 100644 index 00000000..c27e2a80 --- /dev/null +++ b/daemon/ui/config/config.go @@ -0,0 +1,54 @@ +package config + +import ( + "sync" + + "github.com/evilsocket/opensnitch/daemon/log/loggers" + "github.com/evilsocket/opensnitch/daemon/statistics" +) + +type serverTLSOptions struct { + CACert string `json:"CACert"` + ServerCert string `json:"ServerCert"` + ServerKey string `json:"ServerKey"` + ClientCert string `json:"ClientCert"` + ClientKey string `json:"ClientKey"` + // https://pkg.go.dev/crypto/tls#Config + SkipVerify bool `json:"SkipVerify"` + //https://pkg.go.dev/crypto/tls#ClientAuthType + ClientAuthType string `json:"ClientAuthType"` + + // https://pkg.go.dev/crypto/tls#Conn.VerifyHostname + //VerifyHostname bool + // https://pkg.go.dev/crypto/tls#example-Config-VerifyConnection + // VerifyConnection bool + // VerifyPeerCertificate bool +} + +type serverAuth struct { + // token?, google?, simple-tls, mutual-tls + Type string `json:"Type"` + TLSOptions serverTLSOptions `json:"TLSOptions"` +} + +type serverConfig struct { + Address string `json:"Address"` + Authentication serverAuth `json:"Authentication"` + LogFile string `json:"LogFile"` + Loggers []loggers.LoggerConfig `json:"Loggers"` +} + +// Config holds the values loaded from configFile +type Config struct { + sync.RWMutex + Server serverConfig `json:"Server"` + DefaultAction string `json:"DefaultAction"` + DefaultDuration string `json:"DefaultDuration"` + InterceptUnknown bool `json:"InterceptUnknown"` + ProcMonitorMethod string `json:"ProcMonitorMethod"` + LogLevel *uint32 `json:"LogLevel"` + LogUTC bool `json:"LogUTC"` + LogMicro bool `json:"LogMicro"` + Firewall string `json:"Firewall"` + Stats statistics.StatsConfig `json:"Stats"` +} diff --git a/daemon/ui/config.go b/daemon/ui/config_utils.go similarity index 64% rename from daemon/ui/config.go rename to daemon/ui/config_utils.go index cdc372d7..b4422648 100644 --- a/daemon/ui/config.go +++ b/daemon/ui/config_utils.go @@ -10,6 +10,7 @@ import ( "github.com/evilsocket/opensnitch/daemon/log" "github.com/evilsocket/opensnitch/daemon/procmon/monitor" "github.com/evilsocket/opensnitch/daemon/rule" + "github.com/evilsocket/opensnitch/daemon/ui/config" ) func (c *Client) getSocketPath(socketPath string) string { @@ -33,13 +34,13 @@ func (c *Client) setSocketPath(socketPath string) { } func (c *Client) isProcMonitorEqual(newMonitorMethod string) bool { - config.RLock() - defer config.RUnlock() + clientConfig.RLock() + defer clientConfig.RUnlock() - return newMonitorMethod == config.ProcMonitorMethod + return newMonitorMethod == clientConfig.ProcMonitorMethod } -func (c *Client) parseConf(rawConfig string) (conf Config, err error) { +func (c *Client) parseConf(rawConfig string) (conf config.Config, err error) { err = json.Unmarshal([]byte(rawConfig), &conf) return conf, err } @@ -68,45 +69,45 @@ func (c *Client) loadDiskConfiguration(reload bool) { } func (c *Client) loadConfiguration(rawConfig []byte) bool { - config.Lock() - defer config.Unlock() + clientConfig.Lock() + defer clientConfig.Unlock() - if err := json.Unmarshal(rawConfig, &config); err != nil { + if err := json.Unmarshal(rawConfig, &clientConfig); err != nil { msg := fmt.Sprintf("Error parsing configuration %s: %s", configFile, err) log.Error(msg) c.SendWarningAlert(msg) return false } // firstly load config level, to detect further errors if any - if config.LogLevel != nil { - log.SetLogLevel(int(*config.LogLevel)) + if clientConfig.LogLevel != nil { + log.SetLogLevel(int(*clientConfig.LogLevel)) } - log.SetLogUTC(config.LogUTC) - log.SetLogMicro(config.LogMicro) - if config.Server.LogFile != "" { + log.SetLogUTC(clientConfig.LogUTC) + log.SetLogMicro(clientConfig.LogMicro) + if clientConfig.Server.LogFile != "" { log.Close() - log.OpenFile(config.Server.LogFile) + log.OpenFile(clientConfig.Server.LogFile) } - if config.Server.Address != "" { - tempSocketPath := c.getSocketPath(config.Server.Address) + if clientConfig.Server.Address != "" { + tempSocketPath := c.getSocketPath(clientConfig.Server.Address) if tempSocketPath != c.socketPath { // disconnect, and let the connection poller reconnect to the new address c.disconnect() } c.setSocketPath(tempSocketPath) } - if config.DefaultAction != "" { - clientDisconnectedRule.Action = rule.Action(config.DefaultAction) - clientErrorRule.Action = rule.Action(config.DefaultAction) + if clientConfig.DefaultAction != "" { + clientDisconnectedRule.Action = rule.Action(clientConfig.DefaultAction) + clientErrorRule.Action = rule.Action(clientConfig.DefaultAction) } - if config.DefaultDuration != "" { - clientDisconnectedRule.Duration = rule.Duration(config.DefaultDuration) - clientErrorRule.Duration = rule.Duration(config.DefaultDuration) + if clientConfig.DefaultDuration != "" { + clientDisconnectedRule.Duration = rule.Duration(clientConfig.DefaultDuration) + clientErrorRule.Duration = rule.Duration(clientConfig.DefaultDuration) } - if config.ProcMonitorMethod != "" { - if err := monitor.ReconfigureMonitorMethod(config.ProcMonitorMethod); err != nil { - msg := fmt.Sprintf("Unable to set new process monitor (%s) method from disk: %v", config.ProcMonitorMethod, err) + if clientConfig.ProcMonitorMethod != "" { + if err := monitor.ReconfigureMonitorMethod(clientConfig.ProcMonitorMethod); err != nil { + msg := fmt.Sprintf("Unable to set new process monitor (%s) method from disk: %v", clientConfig.ProcMonitorMethod, err) log.Warning(msg) c.SendWarningAlert(msg) } diff --git a/ui/bin/opensnitch-ui b/ui/bin/opensnitch-ui index 7fc5a027..fb4e5e68 100755 --- a/ui/bin/opensnitch-ui +++ b/ui/bin/opensnitch-ui @@ -42,6 +42,7 @@ from opensnitch.utils import Themes, Utils, Versions, Message from opensnitch.utils.xdg import xdg_runtime_dir import opensnitch.ui_pb2 from opensnitch.ui_pb2_grpc import add_UIServicer_to_server +from opensnitch import auth def on_exit(): server.stop(0) @@ -69,6 +70,9 @@ Examples: * Use unix:///run/1000/YOUR_USER/opensnitch/osui.sock for better privacy. - Listening on port 50051, all interfaces: opensnitch-ui --socket "[::]:50051" ''', metavar="FILE") + parser.add_argument("--socket-auth", dest="socket_auth", help="Auth type: simple, tls-simple, tls-mutual") + parser.add_argument("--tls-cert", dest="tls_cert", help="path to the server cert") + parser.add_argument("--tls-key", dest="tls_key", help="path to the server key") parser.add_argument("--max-clients", dest="serverWorkers", default=10, help="Max number of allowed clients (incoming connections).") parser.add_argument("--debug", dest="debug", action="store_true", help="Enable debug logs") parser.add_argument("--debug-grpc", dest="debug_grpc", action="store_true", help="Enable gRPC debug logs") @@ -97,72 +101,90 @@ Examples: app = QtWidgets.QApplication(sys.argv) lockfile = QtCore.QLockFile(os.path.join(xdg_runtime_dir, 'osui.lock')) - if lockfile.tryLock(100): - if hasattr(QtCore.Qt, 'AA_UseHighDpiPixmaps'): - app.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True) - thm = Themes.instance() - thm.load_theme(app) - - Utils.create_socket_dirs() - cfg = Config.get() - if args.socket == None: - # default - args.socket = "unix:///tmp/osui.sock" - - addr = cfg.getSettings(Config.DEFAULT_SERVER_ADDR) - if addr != None and addr != "": - if addr.startswith("unix://"): - if not os.path.exists(os.path.dirname(addr[7:])): - print("WARNING: unix socket path does not exist, using unix:///tmp/osui.sock, ", addr) - else: - args.socket = addr - else: - args.socket = addr - - print("Using server address:", args.socket) - - maxmsglen = cfg.getMaxMsgLength() - - service = UIService(app, on_exit) - # @doc: https://grpc.github.io/grpc/python/grpc.html#server-object - server = grpc.server(futures.ThreadPoolExecutor(), - options=( - # https://github.com/grpc/grpc/blob/master/doc/keepalive.md - # https://grpc.github.io/grpc/core/group__grpc__arg__keys.html - # send keepalive ping every 5 second, default is 2 hours) - ('grpc.keepalive_time_ms', 5000), - # after 5s of inactivity, wait 20s and close the connection if - # there's no response. - ('grpc.keepalive_timeout_ms', 20000), - ('grpc.keepalive_permit_without_calls', True), - ('grpc.max_send_message_length', maxmsglen), - ('grpc.max_receive_message_length', maxmsglen), - )) - - add_UIServicer_to_server(service, server) - - if args.socket.startswith("unix://"): - socket = args.socket[7:] - socket = os.path.abspath(socket) - server.add_insecure_port("unix:%s" % socket) - else: - server.add_insecure_port(args.socket) - - # https://stackoverflow.com/questions/5160577/ctrl-c-doesnt-work-with-pyqt - signal.signal(signal.SIGINT, signal.SIG_DFL) - - # print "OpenSnitch UI service running on %s ..." % socket - server.start() - app.exec_() - - else: + if not lockfile.tryLock(100): Message.ok( QC.translate("stats", "Error"), QC.translate("stats", "OpenSnitch UI is already running!"), QtWidgets.QMessageBox.Warning ) + raise Exception("GUI already running, exiting.") + + if hasattr(QtCore.Qt, 'AA_UseHighDpiPixmaps'): + app.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True) + thm = Themes.instance() + thm.load_theme(app) + + Utils.create_socket_dirs() + cfg = Config.get() + if args.socket == None: + # default + args.socket = "unix:///tmp/osui.sock" + + addr = cfg.getSettings(Config.DEFAULT_SERVER_ADDR) + if addr != None and addr != "": + if addr.startswith("unix://"): + if not os.path.exists(os.path.dirname(addr[7:])): + print("WARNING: unix socket path does not exist, using unix:///tmp/osui.sock, ", addr) + else: + args.socket = addr + else: + args.socket = addr + + print("Using server address:", args.socket) + + maxmsglen = cfg.getMaxMsgLength() + + service = UIService(app, on_exit) + # @doc: https://grpc.github.io/grpc/python/grpc.html#server-object + server = grpc.server(futures.ThreadPoolExecutor(), + options=( + # https://github.com/grpc/grpc/blob/master/doc/keepalive.md + # https://grpc.github.io/grpc/core/group__grpc__arg__keys.html + # send keepalive ping every 5 second, default is 2 hours) + ('grpc.keepalive_time_ms', 5000), + # after 5s of inactivity, wait 20s and close the connection if + # there's no response. + ('grpc.keepalive_timeout_ms', 20000), + ('grpc.keepalive_permit_without_calls', True), + ('grpc.max_send_message_length', maxmsglen), + ('grpc.max_receive_message_length', maxmsglen), + )) + + add_UIServicer_to_server(service, server) + + if args.socket.startswith("unix://"): + socket = args.socket[7:] + socket = os.path.abspath(socket) + server.add_insecure_port("unix:%s" % socket) + #server.add_secure_port("unix:%s" % socket, tls_creds) + else: + auth_type = auth.Simple + if args.socket_auth != None: + auth_type = args.socket_auth + elif cfg.getSettings(Config.AUTH_TYPE) != None: + auth_type = cfg.getSettings(Config.AUTH_TYPE) + + + if auth_type == auth.Simple or auth_type == "": + server.add_insecure_port(args.socket) + else: + auth_cert = cfg.getSettings(Config.AUTH_CERT) + auth_certkey = cfg.getSettings(Config.AUTH_CERTKEY) + tls_creds = auth.get_tls_credentials(args.tls_cert, args.tls_key) + if tls_creds == None: + raise Exception("Invalid TLS credentials. Review the server key and cert files.") + server.add_secure_port(args.socket, tls_creds) + + # https://stackoverflow.com/questions/5160577/ctrl-c-doesnt-work-with-pyqt + signal.signal(signal.SIGINT, signal.SIG_DFL) + + # print "OpenSnitch UI service running on %s ..." % socket + server.start() + app.exec_() except KeyboardInterrupt: on_exit() + except Exception as e: + print(e) finally: lockfile.unlock()