diff --git a/configs/dbus/com.telekom_mms.oc_daemon.Daemon.conf b/configs/dbus/com.telekom_mms.oc_daemon.Daemon.conf new file mode 100644 index 0000000..b0ca5cd --- /dev/null +++ b/configs/dbus/com.telekom_mms.oc_daemon.Daemon.conf @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/configs/dbus/com.telekom_mms.oc_daemon.Daemon.service b/configs/dbus/com.telekom_mms.oc_daemon.Daemon.service new file mode 100644 index 0000000..9d5ab5d --- /dev/null +++ b/configs/dbus/com.telekom_mms.oc_daemon.Daemon.service @@ -0,0 +1,5 @@ +[D-BUS Service] +Name=com.telekom_mms.oc_daemon.Daemon +Exec=/bin/false +# alias for systemd service, do not use? +# SystemdService=dbus-com.telekom_mms.oc_daemon.Daemon.service diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index 6494a33..534b248 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -10,6 +10,7 @@ import ( "time" "github.com/T-Systems-MMS/oc-daemon/internal/api" + "github.com/T-Systems-MMS/oc-daemon/internal/dbusapi" "github.com/T-Systems-MMS/oc-daemon/internal/dnsproxy" "github.com/T-Systems-MMS/oc-daemon/internal/ocrunner" "github.com/T-Systems-MMS/oc-daemon/internal/sleepmon" @@ -46,6 +47,7 @@ var ( // Daemon is used to run the daemon type Daemon struct { server *api.Server + dbus *dbusapi.Service dns *dnsproxy.Proxy tnd *trustnet.TND @@ -90,6 +92,7 @@ func (d *Daemon) setStatusTrustedNetwork(trusted bool) { // status changed d.status.TrustedNetwork = trustedNetwork + d.dbus.SetProperty(dbusapi.PropertyTrustedNetwork, trustedNetwork) } // setStatusConnectionState sets the connection state in status @@ -101,6 +104,7 @@ func (d *Daemon) setStatusConnectionState(connectionState vpnstatus.ConnectionSt // state changed d.status.ConnectionState = connectionState + d.dbus.SetProperty(dbusapi.PropertyConnectionState, connectionState) } // setStatusIP sets the IP in status @@ -112,6 +116,7 @@ func (d *Daemon) setStatusIP(ip string) { // ip changed d.status.IP = ip + d.dbus.SetProperty(dbusapi.PropertyIP, ip) } // setStatusDevice sets the device in status @@ -123,6 +128,7 @@ func (d *Daemon) setStatusDevice(device string) { // device changed d.status.Device = device + d.dbus.SetProperty(dbusapi.PropertyDevice, device) } // setStatusConnectedAt sets the connection time in status @@ -134,6 +140,7 @@ func (d *Daemon) setStatusConnectedAt(connectedAt int64) { // connection time changed d.status.ConnectedAt = connectedAt + d.dbus.SetProperty(dbusapi.PropertyConnectedAt, connectedAt) } // setStatusServers sets the vpn servers in status @@ -145,6 +152,7 @@ func (d *Daemon) setStatusServers(servers []string) { // servers changed d.status.Servers = servers + d.dbus.SetProperty(dbusapi.PropertyServers, servers) } // setStatusOCRunning sets the openconnect running state in status @@ -409,6 +417,37 @@ func (d *Daemon) handleClientRequest(request *api.Request) { } } +// handleDBusRequest handles a D-Bus API client request +func (d *Daemon) handleDBusRequest(request *dbusapi.Request) { + defer request.Close() + log.Debug("Daemon handling D-Bus client request") + + switch request.Name { + case dbusapi.RequestConnect: + // create login info + cookie := request.Parameters[0].(string) + host := request.Parameters[1].(string) + connectURL := request.Parameters[2].(string) + fingerprint := request.Parameters[3].(string) + resolve := request.Parameters[4].(string) + + login := &ocrunner.LoginInfo{ + Cookie: cookie, + Host: host, + ConnectURL: connectURL, + Fingerprint: fingerprint, + Resolve: resolve, + } + + // connect VPN + d.connectVPN(login) + + case dbusapi.RequestDisconnect: + // diconnect VPN + d.disconnectVPN() + } +} + // handleDNSReport handles a DNS report func (d *Daemon) handleDNSReport(r *dnsproxy.Report) { log.WithField("report", r).Debug("Daemon handling DNS report") @@ -689,6 +728,12 @@ func (d *Daemon) start() { d.server.Start() defer d.server.Stop() + // start dbus api service + d.dbus.Start() + defer d.dbus.Stop() + d.setStatusConnectionState(vpnstatus.ConnectionStateDisconnected) + d.setStatusServers(d.profile.GetVPNServers()) + // start xml profile watching d.profile.Start() defer d.profile.Stop() @@ -699,6 +744,9 @@ func (d *Daemon) start() { case req := <-d.server.Requests(): d.handleClientRequest(req) + case req := <-d.dbus.Requests(): + d.handleDBusRequest(req) + case r := <-d.dns.Reports(): d.handleDNSReport(r) @@ -740,6 +788,7 @@ func NewDaemon() *Daemon { return &Daemon{ server: api.NewServer(sockFile), + dbus: dbusapi.NewService(), sleepmon: sleepmon.NewSleepMon(), diff --git a/internal/dbusapi/service.go b/internal/dbusapi/service.go new file mode 100644 index 0000000..8c532c6 --- /dev/null +++ b/internal/dbusapi/service.go @@ -0,0 +1,332 @@ +package dbusapi + +import ( + "errors" + + "github.com/godbus/dbus/v5" + "github.com/godbus/dbus/v5/introspect" + "github.com/godbus/dbus/v5/prop" + log "github.com/sirupsen/logrus" +) + +// D-Bus object path and interface +const ( + Path = "/com/telekom_mms/oc_daemon/Daemon" + Interface = "com.telekom_mms.oc_daemon.Daemon" +) + +// Properties +const ( + PropertyTrustedNetwork = "TrustedNetwork" + PropertyConnectionState = "ConnectionState" + PropertyIP = "IP" + PropertyDevice = "Device" + PropertyConnectedAt = "ConnectedAt" + PropertyServers = "Servers" +) + +// Property "Trusted Network" states +const ( + TrustedNetworkUnknown uint32 = iota + TrustedNetworkNotTrusted + TrustedNetworkTrusted +) + +// Property "Connection State" states +const ( + ConnectionStateUnknown uint32 = iota + ConnectionStateDisconnected + ConnectionStateConnecting + ConnectionStateConnected + ConnectionStateDisconnecting +) + +// Property "IP" values +const ( + IPInvalid = "" +) + +// Property "Device" values +const ( + DeviceInvalid = "" +) + +// Property "Connected At" values +const ( + ConnectedAtInvalid int64 = -1 +) + +// Property "Servers" values +var ( + ServersInvalid []string +) + +// Request Names +const ( + RequestConnect = "Connect" + RequestDisconnect = "Disconnect" +) + +// Request is a D-Bus client request +type Request struct { + Name string + Parameters []any + Results []any + Error error + + wait chan struct{} + done chan struct{} +} + +// Close completes the request handling +func (r *Request) Close() { + close(r.wait) +} + +// Wait waits for the completion of request handling +func (r *Request) Wait() { + select { + case <-r.wait: + case <-r.done: + r.Error = errors.New("Request aborted") + } +} + +// daemon defines daemon interface methods +type daemon struct { + requests chan *Request + done chan struct{} +} + +// Connect is the "Connect" method of the D-Bus interface +func (d daemon) Connect(sender dbus.Sender, cookie, host, connectURL, fingerprint, resolve string) *dbus.Error { + log.WithField("sender", sender).Debug("Received D-Bus Connect() call") + request := &Request{ + Name: RequestConnect, + Parameters: []any{cookie, host, connectURL, fingerprint, resolve}, + wait: make(chan struct{}), + done: d.done, + } + select { + case d.requests <- request: + case <-d.done: + return dbus.NewError(Interface+".ConnectAborted", []any{"Connect aborted"}) + } + + request.Wait() + if request.Error != nil { + return dbus.NewError(Interface+".ConnectAborted", []any{request.Error.Error()}) + } + return nil +} + +// Disconnect is the "Disconnect" method of the D-Bus interface +func (d daemon) Disconnect(sender dbus.Sender) *dbus.Error { + log.WithField("sender", sender).Debug("Received D-Bus Connect() call") + request := &Request{ + Name: RequestDisconnect, + wait: make(chan struct{}), + done: d.done, + } + select { + case d.requests <- request: + case <-d.done: + return dbus.NewError(Interface+".DisconnectAborted", []any{"Disconnect aborted"}) + } + + request.Wait() + if request.Error != nil { + return dbus.NewError(Interface+".DisconnectAborted", []any{request.Error.Error()}) + } + return nil +} + +// propertyUpdate is an update of a property +type propertyUpdate struct { + name string + value any +} + +// Service is a D-Bus Service +type Service struct { + requests chan *Request + propUps chan *propertyUpdate + done chan struct{} + closed chan struct{} +} + +// dbusConn is an interface for dbus.Conn to allow for testing +type dbusConn interface { + Close() error + Export(v any, path dbus.ObjectPath, iface string) error + RequestName(name string, flags dbus.RequestNameFlags) (dbus.RequestNameReply, error) +} + +// dbusConnectSystemBus encapsulates dbus.ConnectSystemBus to allow for testing +var dbusConnectSystemBus = func(opts ...dbus.ConnOption) (dbusConn, error) { + return dbus.ConnectSystemBus(opts...) +} + +// propProperties is an interface for prop.Properties to allow for testing +type propProperties interface { + Introspection(iface string) []introspect.Property + SetMust(iface, property string, v any) +} + +// propExport encapsulates prop.Export to allow for testing +var propExport = func(conn dbusConn, path dbus.ObjectPath, props prop.Map) (propProperties, error) { + return prop.Export(conn.(*dbus.Conn), path, props) +} + +// start starts the service +func (s *Service) start() { + defer close(s.closed) + + // connect to session bus + conn, err := dbusConnectSystemBus() + if err != nil { + log.WithError(err).Fatal("Could not connect to D-Bus session bus") + } + defer func() { _ = conn.Close() }() + + // request name + reply, err := conn.RequestName(Interface, dbus.NameFlagDoNotQueue) + if err != nil { + log.WithError(err).Fatal("Could not request D-Bus name") + } + if reply != dbus.RequestNameReplyPrimaryOwner { + log.Fatal("Requested D-Bus name is already taken") + } + + // methods + meths := daemon{s.requests, s.done} + err = conn.Export(meths, Path, Interface) + if err != nil { + log.WithError(err).Fatal("Could not export D-Bus methods") + } + + // properties + propsSpec := prop.Map{ + Interface: { + PropertyTrustedNetwork: { + Value: TrustedNetworkUnknown, + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + PropertyConnectionState: { + Value: ConnectionStateUnknown, + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + PropertyIP: { + Value: IPInvalid, + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + PropertyDevice: { + Value: DeviceInvalid, + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + PropertyConnectedAt: { + Value: ConnectedAtInvalid, + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + PropertyServers: { + Value: ServersInvalid, + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + }, + } + props, err := propExport(conn, Path, propsSpec) + if err != nil { + log.WithError(err).Fatal("Could not export D-Bus properties spec") + } + + // introspection + n := &introspect.Node{ + Name: Path, + Interfaces: []introspect.Interface{ + introspect.IntrospectData, + prop.IntrospectData, + { + Name: Interface, + Methods: introspect.Methods(meths), + Properties: props.Introspection(Interface), + }, + }, + } + err = conn.Export(introspect.NewIntrospectable(n), Path, + "org.freedesktop.DBus.Introspectable") + if err != nil { + log.WithError(err).Fatal("Could not export D-Bus introspection") + } + + // set properties values to emit properties changed signal and make + // sure existing clients get updated values after restart + props.SetMust(Interface, PropertyTrustedNetwork, TrustedNetworkNotTrusted) + props.SetMust(Interface, PropertyConnectionState, ConnectionStateDisconnected) + props.SetMust(Interface, PropertyIP, IPInvalid) + props.SetMust(Interface, PropertyDevice, DeviceInvalid) + props.SetMust(Interface, PropertyConnectedAt, ConnectedAtInvalid) + props.SetMust(Interface, PropertyServers, ServersInvalid) + + // main loop + for { + select { + case u := <-s.propUps: + // update property + log.WithFields(log.Fields{ + "name": u.name, + "value": u.value, + }).Debug("D-Bus updating property") + props.SetMust(Interface, u.name, u.value) + + case <-s.done: + log.Debug("D-Bus service stopping") + return + } + } +} + +// Start starts the service +func (s *Service) Start() { + go s.start() +} + +// Stop stops the service +func (s *Service) Stop() { + close(s.done) + <-s.closed +} + +// Requests returns the requests channel of service +func (s *Service) Requests() chan *Request { + return s.requests +} + +// SetProperty sets property with name to value +func (s *Service) SetProperty(name string, value any) { + select { + case s.propUps <- &propertyUpdate{name, value}: + case <-s.done: + } +} + +// NewService returns a new service +func NewService() *Service { + return &Service{ + requests: make(chan *Request), + propUps: make(chan *propertyUpdate), + done: make(chan struct{}), + closed: make(chan struct{}), + } +} diff --git a/internal/dbusapi/service_test.go b/internal/dbusapi/service_test.go new file mode 100644 index 0000000..bd99482 --- /dev/null +++ b/internal/dbusapi/service_test.go @@ -0,0 +1,203 @@ +package dbusapi + +import ( + "reflect" + "testing" + + "github.com/godbus/dbus/v5" + "github.com/godbus/dbus/v5/introspect" + "github.com/godbus/dbus/v5/prop" +) + +// TestRequestWaitClose tests Wait and Close of Request +func TestRequestWaitClose(_ *testing.T) { + // test closing + r := Request{ + Name: "test1", + wait: make(chan struct{}), + done: make(chan struct{}), + } + go func() { + r.Close() + }() + r.Wait() + + // test aborting + done := make(chan struct{}) + r = Request{ + Name: "test2", + wait: make(chan struct{}), + done: done, + } + go func() { + close(done) + }() + r.Wait() +} + +// TestDaemonConnect tests Connect of daemon +func TestDaemonConnect(t *testing.T) { + // create daemon + requests := make(chan *Request) + done := make(chan struct{}) + daemon := daemon{ + requests: requests, + done: done, + } + + // run connect and get results + cookie, host, connectURL, fingerprint, resolve := + "cookie", "host", "connectURL", "fingerprint", "resolve" + want := &Request{ + Name: RequestConnect, + Parameters: []any{cookie, host, connectURL, fingerprint, resolve}, + done: done, + } + got := &Request{} + go func() { + r := <-requests + got = r + r.Close() + }() + err := daemon.Connect("sender", cookie, host, connectURL, fingerprint, resolve) + if err != nil { + t.Error(err) + } + + // check results + if got.Name != want.Name || + !reflect.DeepEqual(got.Parameters, want.Parameters) || + !reflect.DeepEqual(got.Results, want.Results) || + got.Error != want.Error || + got.done != want.done { + // not equal + t.Errorf("got %v, want %v", got, want) + } +} + +// TestDaemonDisconnect tests Disconnect of daemon +func TestDaemonDisconnect(t *testing.T) { + // create daemon + requests := make(chan *Request) + done := make(chan struct{}) + daemon := daemon{ + requests: requests, + done: done, + } + + // run disconnect and get results + want := &Request{ + Name: RequestDisconnect, + done: done, + } + got := &Request{} + go func() { + r := <-requests + got = r + r.Close() + }() + err := daemon.Disconnect("sender") + if err != nil { + t.Error(err) + } + + // check results + if got.Name != want.Name || + !reflect.DeepEqual(got.Parameters, want.Parameters) || + !reflect.DeepEqual(got.Results, want.Results) || + got.Error != want.Error || + got.done != want.done { + // not equal + t.Errorf("got %v, want %v", got, want) + } +} + +// testConn implements the dbusConn interface for testing +type testConn struct{} + +func (tc *testConn) Close() error { + return nil +} + +func (tc *testConn) Export(any, dbus.ObjectPath, string) error { + return nil +} + +func (tc *testConn) RequestName(string, dbus.RequestNameFlags) (dbus.RequestNameReply, error) { + return dbus.RequestNameReplyPrimaryOwner, nil +} + +// testProperties implements the propProperties interface for testing +type testProperties struct { + props map[string]any +} + +func (tp *testProperties) Introspection(string) []introspect.Property { + return nil +} + +func (tp *testProperties) SetMust(_, property string, v any) { + if tp.props == nil { + // props not set, skip + return + } + + // ignore iface, map property to value + tp.props[property] = v +} + +// TestServiceStartStop tests Start and Stop of Service +func TestServiceStartStop(_ *testing.T) { + dbusConnectSystemBus = func(opts ...dbus.ConnOption) (dbusConn, error) { + return &testConn{}, nil + } + propExport = func(conn dbusConn, path dbus.ObjectPath, props prop.Map) (propProperties, error) { + return &testProperties{}, nil + } + s := NewService() + s.Start() + s.Stop() +} + +// TestServiceRequests tests Requests of Service +func TestServiceRequests(t *testing.T) { + s := NewService() + want := s.requests + got := s.Requests() + if got != want { + t.Errorf("got %v, want %v", got, want) + } +} + +// TestServiceSetProperty tests SetProperty of Service +func TestServiceSetProperty(t *testing.T) { + dbusConnectSystemBus = func(opts ...dbus.ConnOption) (dbusConn, error) { + return &testConn{}, nil + } + properties := &testProperties{props: make(map[string]any)} + propExport = func(conn dbusConn, path dbus.ObjectPath, props prop.Map) (propProperties, error) { + return properties, nil + } + s := NewService() + s.Start() + + propName := "test-property" + want := "test-value" + + s.SetProperty(propName, want) + s.Stop() + + got := properties.props[propName] + if got != want { + t.Errorf("got %s, want %s", got, want) + } +} + +// TestNewService tests NewService +func TestNewService(t *testing.T) { + s := NewService() + empty := &Service{} + if reflect.DeepEqual(s, empty) { + t.Errorf("got empty, want not empty") + } +} diff --git a/tools/dbusclient/main.go b/tools/dbusclient/main.go new file mode 100644 index 0000000..bd7425a --- /dev/null +++ b/tools/dbusclient/main.go @@ -0,0 +1,141 @@ +package main + +import ( + "fmt" + + "github.com/T-Systems-MMS/oc-daemon/internal/dbusapi" + "github.com/godbus/dbus/v5" + log "github.com/sirupsen/logrus" +) + +func main() { + // connect to session bus + conn, err := dbus.ConnectSystemBus() + if err != nil { + log.Fatal(err) + } + defer func() { _ = conn.Close() }() + + // subscribe to properties changed signals + if err = conn.AddMatchSignal( + dbus.WithMatchSender(dbusapi.Interface), + dbus.WithMatchInterface("org.freedesktop.DBus.Properties"), + dbus.WithMatchMember("PropertiesChanged"), + dbus.WithMatchPathNamespace(dbusapi.Path), + ); err != nil { + log.Fatal(err) + } + + // get initial values of properties + trustedNetwork := dbusapi.TrustedNetworkUnknown + connectionState := dbusapi.ConnectionStateUnknown + ip := dbusapi.IPInvalid + device := dbusapi.DeviceInvalid + connectedAt := dbusapi.ConnectedAtInvalid + servers := dbusapi.ServersInvalid + + getProperty := func(name string, val any) { + err = conn.Object(dbusapi.Interface, dbusapi.Path). + StoreProperty(dbusapi.Interface+"."+name, val) + if err != nil { + log.Fatal(err) + } + } + getProperty(dbusapi.PropertyTrustedNetwork, &trustedNetwork) + getProperty(dbusapi.PropertyConnectionState, &connectionState) + getProperty(dbusapi.PropertyIP, &ip) + getProperty(dbusapi.PropertyDevice, &device) + getProperty(dbusapi.PropertyConnectedAt, &connectedAt) + getProperty(dbusapi.PropertyServers, &servers) + + log.Println("TrustedNetwork:", trustedNetwork) + log.Println("ConnectionState:", connectionState) + log.Println("IP:", ip) + log.Println("Device:", device) + log.Println("ConnectedAt:", connectedAt) + log.Println("Servers:", servers) + + // handle signals + c := make(chan *dbus.Signal, 10) + conn.Signal(c) + for s := range c { + // make sure it's a properties changed signal + if s.Path != dbusapi.Path || + s.Name != "org.freedesktop.DBus.Properties.PropertiesChanged" { + log.Error("Not a properties changed signal") + continue + } + + // check properties changed signal + if v, ok := s.Body[0].(string); !ok || v != dbusapi.Interface { + log.Error("Not the right properties changed signal") + continue + } + + // get changed properties + changed, ok := s.Body[1].(map[string]dbus.Variant) + if !ok { + log.Error("Invalid changed properties in properties changed signal") + continue + } + for name, value := range changed { + fmt.Printf("Changed property: %s ", name) + switch name { + case dbusapi.PropertyTrustedNetwork: + if err := value.Store(&trustedNetwork); err != nil { + log.Fatal(err) + } + fmt.Println(trustedNetwork) + case dbusapi.PropertyConnectionState: + if err := value.Store(&connectionState); err != nil { + log.Fatal(err) + } + fmt.Println(connectionState) + case dbusapi.PropertyIP: + if err := value.Store(&ip); err != nil { + log.Fatal(err) + } + fmt.Println(ip) + case dbusapi.PropertyDevice: + if err := value.Store(&device); err != nil { + log.Fatal(err) + } + fmt.Println(device) + case dbusapi.PropertyConnectedAt: + if err := value.Store(&connectedAt); err != nil { + log.Fatal(err) + } + fmt.Println(connectedAt) + case dbusapi.PropertyServers: + if err := value.Store(&servers); err != nil { + log.Fatal(err) + } + fmt.Println(servers) + } + } + + // get invalidated properties + invalid, ok := s.Body[2].([]string) + if !ok { + log.Error("Invalid invalidated properties in properties changed signal") + } + for _, name := range invalid { + // not expected to happen currently, but handle it anyway + switch name { + case dbusapi.PropertyTrustedNetwork: + trustedNetwork = dbusapi.TrustedNetworkUnknown + case dbusapi.PropertyConnectionState: + connectionState = dbusapi.ConnectionStateUnknown + case dbusapi.PropertyIP: + ip = dbusapi.IPInvalid + case dbusapi.PropertyDevice: + device = dbusapi.DeviceInvalid + case dbusapi.PropertyConnectedAt: + connectedAt = dbusapi.ConnectedAtInvalid + case dbusapi.PropertyServers: + servers = dbusapi.ServersInvalid + } + fmt.Printf("Invalidated property: %s\n", name) + } + } +}