diff --git a/go.mod b/go.mod index bfb1ac0..05ef098 100644 --- a/go.mod +++ b/go.mod @@ -13,4 +13,5 @@ golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210506160403-92e472f520a5 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + gorm.io/gorm v1.21.14 ) diff --git a/internal/wireguard/configuration.go b/internal/wireguard/configuration.go index a05b33a..83c3d68 100644 --- a/internal/wireguard/configuration.go +++ b/internal/wireguard/configuration.go @@ -19,6 +19,13 @@ return o.Value.(string) } +func NewStringConfigOption(value string, overridable bool) StringConfigOption { + return StringConfigOption{ConfigOption{ + Value: value, + Overridable: overridable, + }} +} + type IntConfigOption struct { ConfigOption } @@ -27,6 +34,13 @@ return o.Value.(int) } +func NewIntConfigOption(value int, overridable bool) IntConfigOption { + return IntConfigOption{ConfigOption{ + Value: value, + Overridable: overridable, + }} +} + type Int32ConfigOption struct { ConfigOption } @@ -35,6 +49,13 @@ return o.Value.(int32) } +func NewInt32ConfigOption(value int32, overridable bool) Int32ConfigOption { + return Int32ConfigOption{ConfigOption{ + Value: value, + Overridable: overridable, + }} +} + type BoolConfigOption struct { ConfigOption } @@ -43,6 +64,13 @@ return o.Value.(bool) } +func NewBoolConfigOption(value bool, overridable bool) BoolConfigOption { + return BoolConfigOption{ConfigOption{ + Value: value, + Overridable: overridable, + }} +} + type InterfaceType string const ( @@ -135,19 +163,16 @@ UpdatedAt time.Time } -type InterfaceConfigPersister interface { - PersistInterface(cfg InterfaceConfig) - LoadInterface(cfg InterfaceConfig) - DeleteInterface(cfg InterfaceConfig) +// ConfigWriter provides methods for updating persistent backends (like a database or a WireGuard configuration file) +type ConfigWriter interface { + SaveInterface(cfg InterfaceConfig, peers []PeerConfig) error + SavePeer(peer PeerConfig, cfg InterfaceConfig) error + DeleteInterface(cfg InterfaceConfig, peers []PeerConfig) error + DeletePeer(peer PeerConfig, cfg InterfaceConfig) error } -type PeerConfigPersister interface { - PersistPeer(cfg PeerConfig) - LoadPeer(cfg PeerConfig) - DeletePeer(cfg PeerConfig) -} - -type ConfigPersister interface { - InterfaceConfigPersister - PeerConfigPersister +// ConfigLoader provides methods to load interface and peer configurations from a persistent backend. +type ConfigLoader interface { + Load(identifier DeviceIdentifier) (InterfaceConfig, []PeerConfig, error) + LoadAll() (map[InterfaceConfig][]PeerConfig, error) } diff --git a/internal/wireguard/manager.go b/internal/wireguard/manager.go index d387c90..0831e71 100644 --- a/internal/wireguard/manager.go +++ b/internal/wireguard/manager.go @@ -5,9 +5,9 @@ "fmt" "net" "os" - "path/filepath" "strconv" "strings" + "sync" "time" "github.com/pkg/errors" @@ -40,11 +40,14 @@ } type ManagementUtil struct { - configPath string + mux sync.RWMutex // mutex to synchronize access to maps wg Client nl NetlinkClient - cp ConfigPersister + + // config writers and loaders are used to populate the internal config maps + cw []ConfigWriter + cl []ConfigLoader // internal holder of interface configurations interfaces map[DeviceIdentifier]InterfaceConfig @@ -52,7 +55,7 @@ peers map[DeviceIdentifier]map[PeerIdentifier]PeerConfig } -func (m ManagementUtil) GetFreshKeypair() (KeyPair, error) { +func (m *ManagementUtil) GetFreshKeypair() (KeyPair, error) { privateKey, err := wgtypes.GeneratePrivateKey() if err != nil { return KeyPair{}, errors.Wrap(err, "failed to generate private Key") @@ -64,7 +67,7 @@ }, nil } -func (m ManagementUtil) GetPreSharedKey() (PreSharedKey, error) { +func (m *ManagementUtil) GetPreSharedKey() (PreSharedKey, error) { preSharedKey, err := wgtypes.GenerateKey() if err != nil { return "", errors.Wrap(err, "failed to generate pre-shared Key") @@ -74,6 +77,8 @@ } func (m *ManagementUtil) CreateDevice(identifier DeviceIdentifier) error { + m.mux.Lock() + defer m.mux.Unlock() if m.deviceExists(identifier) { return errors.Errorf("device %s already exists", identifier) } @@ -92,12 +97,20 @@ return errors.Wrapf(err, "failed to enable WireGuard interface") } - m.interfaces[identifier] = InterfaceConfig{DeviceName: identifier} + newInterface := InterfaceConfig{DeviceName: identifier} + m.interfaces[identifier] = newInterface + + err = m.persistInterface(identifier, false) + if err != nil { + return errors.Wrapf(err, "failed to persist created interface %s", identifier) + } return nil } func (m *ManagementUtil) DeleteDevice(identifier DeviceIdentifier) error { + m.mux.Lock() + defer m.mux.Unlock() if !m.deviceExists(identifier) { return errors.Errorf("device %s does not exist", identifier) } @@ -111,12 +124,19 @@ return errors.Wrapf(err, "failed to delete WireGuard interface") } + err = m.persistInterface(identifier, true) + if err != nil { + return errors.Wrapf(err, "failed to persist deleted interface %s", identifier) + } + delete(m.interfaces, identifier) return nil } func (m *ManagementUtil) UpdateDevice(identifier DeviceIdentifier, cfg InterfaceConfig) error { + m.mux.Lock() + defer m.mux.Unlock() if !m.deviceExists(identifier) { return errors.Errorf("device %s does not exist", identifier) } @@ -171,10 +191,17 @@ m.interfaces[identifier] = cfg + err = m.persistInterface(identifier, false) + if err != nil { + return errors.Wrapf(err, "failed to persist updated interface %s", identifier) + } + return nil } -func (m ManagementUtil) GetPeers(device DeviceIdentifier) ([]PeerConfig, error) { +func (m *ManagementUtil) GetPeers(device DeviceIdentifier) ([]PeerConfig, error) { + m.mux.RLock() + defer m.mux.RUnlock() if !m.deviceExists(device) { return nil, errors.Errorf("device %s does not exist", device) } @@ -187,7 +214,9 @@ return peers, nil } -func (m ManagementUtil) SavePeers(device DeviceIdentifier, peers ...PeerConfig) error { +func (m *ManagementUtil) SavePeers(device DeviceIdentifier, peers ...PeerConfig) error { + m.mux.Lock() + defer m.mux.Unlock() if !m.deviceExists(device) { return errors.Errorf("device %s does not exist", device) } @@ -206,12 +235,19 @@ } m.peers[device][peer.Uid] = peer + + err = m.persistPeer(peer.Uid, false) + if err != nil { + return errors.Wrapf(err, "failed to persist updated peer %s", peer.Uid) + } } return nil } -func (m ManagementUtil) RemovePeer(device DeviceIdentifier, peer PeerIdentifier) error { +func (m *ManagementUtil) RemovePeer(device DeviceIdentifier, peer PeerIdentifier) error { + m.mux.Lock() + defer m.mux.Unlock() if !m.deviceExists(device) { return errors.Errorf("device %s does not exist", device) } @@ -236,6 +272,11 @@ return errors.Wrapf(err, "could not remove peer %s from WireGuard device %s", peer, device) } + err = m.persistPeer(peer, true) + if err != nil { + return errors.Wrapf(err, "failed to persist deleted peer %s", peer) + } + delete(m.peers[device], peer) return nil @@ -308,14 +349,14 @@ return wgPeer, nil } -func (m ManagementUtil) deviceExists(identifier DeviceIdentifier) bool { +func (m *ManagementUtil) deviceExists(identifier DeviceIdentifier) bool { if _, ok := m.interfaces[identifier]; ok { return true } return false } -func (m ManagementUtil) peerExists(identifier PeerIdentifier) bool { +func (m *ManagementUtil) peerExists(identifier PeerIdentifier) bool { for _, peers := range m.peers { if _, ok := peers[identifier]; ok { return true @@ -325,8 +366,58 @@ return false } +func (m *ManagementUtil) persistInterface(identifier DeviceIdentifier, delete bool) error { + var err error + + device := m.interfaces[identifier] + peers := make([]PeerConfig, 0, len(m.peers[identifier])) + for _, config := range m.peers[identifier] { + peers = append(peers, config) + } + + for _, writer := range m.cw { + if delete { + err = writer.DeleteInterface(device, peers) + } else { + err = writer.SaveInterface(device, peers) + } + if err != nil { + return errors.Wrapf(err, "failed to persist interface %s", identifier) + } + } + + return nil +} + +func (m *ManagementUtil) persistPeer(identifier PeerIdentifier, delete bool) error { + var err error + + var device InterfaceConfig + var peer PeerConfig + for dev, peers := range m.peers { + if p, ok := peers[identifier]; ok { + device = m.interfaces[dev] + peer = p + break + } + } + + for _, writer := range m.cw { + if delete { + err = writer.DeletePeer(peer, device) + } else { + err = writer.SavePeer(peer, device) + } + if err != nil { + return errors.Wrapf(err, "failed to persist peer %s", identifier) + } + } + + return nil +} + // TODO: fix/implement -func (m ManagementUtil) loadExistingInterfaces() ([]InterfaceConfig, error) { +func (m *ManagementUtil) loadExistingInterfaces() ([]InterfaceConfig, error) { devices, err := m.wg.Devices() if err != nil { return nil, errors.Wrapf(err, "failed to get WireGuard device list") @@ -365,8 +456,8 @@ // parseConfigFile parses WireGuard configuration files (INI syntax) and some additional comments in the file // TODO: fix/implement -func (m ManagementUtil) parseConfigFile(interfaceName string) (InterfaceConfig, []PeerConfig, error) { - configFile := filepath.Join(m.configPath, interfaceName+".conf") +func (m *ManagementUtil) parseConfigFile(interfaceName string) (InterfaceConfig, []PeerConfig, error) { + configFile := "TODO" //filepath.Join(m.configPath, interfaceName+".conf") file, err := os.Open(configFile) if err != nil { diff --git a/internal/wireguard/persistant_db.go b/internal/wireguard/persistant_db.go new file mode 100644 index 0000000..736ef26 --- /dev/null +++ b/internal/wireguard/persistant_db.go @@ -0,0 +1,354 @@ +package wireguard + +import ( + "database/sql" + "time" + + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type DatabaseBackend struct { + DB *gorm.DB +} + +func (d DatabaseBackend) SaveInterface(cfg InterfaceConfig, _ []PeerConfig) error { + iface, peerDefaults := convertInterface(cfg) + + if err := d.DB.Save(&iface).Error; err != nil { + return errors.Wrapf(err, "failed to save interface %s to db", cfg.DeviceName) + } + if err := d.DB.Save(&peerDefaults).Error; err != nil { + return errors.Wrapf(err, "failed to save peer defaults of %s to db", cfg.DeviceName) + } + + return nil +} + +func (d DatabaseBackend) SavePeer(cfg PeerConfig, iface InterfaceConfig) error { + peer := convertPeer(cfg, iface.DeviceName) + + if err := d.DB.Save(&peer).Error; err != nil { + return errors.Wrapf(err, "failed to save peer %s to db", cfg.Uid) + } + + return nil +} + +func (d DatabaseBackend) DeleteInterface(cfg InterfaceConfig, _ []PeerConfig) error { + // Delete peers + if err := d.DB.Where("device_name = ?", cfg.DeviceName).Delete(&dbPeerConfig{}).Error; err != nil { + return errors.Wrapf(err, "failed to delete peer for %s from db", cfg.DeviceName) + } + // Delete peer default settings + if err := d.DB.Where("device_name = ?", cfg.DeviceName).Delete(&dbDefaultPeerConfig{}).Error; err != nil { + return errors.Wrapf(err, "failed to delete peer defaults for %s from db", cfg.DeviceName) + } + // Delete interface config + if err := d.DB.Where("device_name = ?", cfg.DeviceName).Delete(&dbInterfaceConfig{}).Error; err != nil { + return errors.Wrapf(err, "failed to delete interface %s from db", cfg.DeviceName) + } + return nil +} + +func (d DatabaseBackend) DeletePeer(cfg PeerConfig, iface InterfaceConfig) error { + err := d.DB.Where("device_name = ? AND uid = ?", iface.DeviceName, cfg.Uid).Delete(&dbPeerConfig{}).Error + if err != nil { + return errors.Wrapf(err, "failed to delete peer %s from db", cfg.Uid) + } + return nil +} + +func (d DatabaseBackend) Load(identifier DeviceIdentifier) (InterfaceConfig, []PeerConfig, error) { + var iface dbInterfaceConfig + var peerDefaults dbDefaultPeerConfig + var peers []dbPeerConfig + + if err := d.DB.Where("device_name = ?", identifier).First(&iface).Error; err != nil { + return InterfaceConfig{}, nil, errors.Wrapf(err, "failed to load interface %s from db", identifier) + } + if err := d.DB.Where("device_name = ?", identifier).First(&peerDefaults).Error; err != nil { + return InterfaceConfig{}, nil, errors.Wrapf(err, "failed to load peer defaults for %s from db", identifier) + } + if err := d.DB.Where("device_name = ?", identifier).Find(&peers).Error; err != nil { + return InterfaceConfig{}, nil, errors.Wrapf(err, "failed to load peers for %s from db", identifier) + } + + interfaceConfig := InterfaceConfig{ + DeviceName: DeviceIdentifier(iface.DeviceName), + KeyPair: KeyPair{PrivateKey: iface.PrivateKey, PublicKey: iface.PublicKey}, + ListenPort: iface.ListenPort, + AddressStr: iface.AddressStr, + Dns: iface.Dns, + Mtu: iface.Mtu, + FirewallMark: int32(iface.FirewallMark), + RoutingTable: iface.RoutingTable, + PreUp: iface.PreUp, + PostUp: iface.PostUp, + PreDown: iface.PreDown, + PostDown: iface.PostDown, + SaveConfig: iface.SaveConfig, + Enabled: iface.Enabled, + DisplayName: iface.DisplayName, + Type: InterfaceType(iface.Type), + DriverType: iface.DriverType, + + PeerDefNetworkStr: peerDefaults.NetworkStr, + PeerDefDns: peerDefaults.Dns, + PeerDefEndpoint: peerDefaults.Endpoint, + PeerDefAllowedIPsString: peerDefaults.AllowedIPsString, + PeerDefMtu: peerDefaults.Mtu, + PeerDefPersistentKeepalive: peerDefaults.PersistentKeepalive, + PeerDefFirewallMark: int32(peerDefaults.FirewallMark), + PeerDefRoutingTable: peerDefaults.RoutingTable, + PeerDefPreUp: peerDefaults.PreUp, + PeerDefPostUp: peerDefaults.PostUp, + PeerDefPreDown: peerDefaults.PreDown, + PeerDefPostDown: peerDefaults.PostDown, + } + peerConfigs := make([]PeerConfig, len(peers)) + for i, peer := range peers { + peerConfigs[i] = PeerConfig{ + Endpoint: NewStringConfigOption(peer.Endpoint, peer.OvrEndpoint), + AllowedIPsString: NewStringConfigOption(peer.AllowedIPsString, peer.OvrAllowedIPsString), + ExtraAllowedIPsString: peer.ExtraAllowedIPsString, + KeyPair: KeyPair{PrivateKey: peer.PrivateKey, PublicKey: peer.PublicKey}, + PresharedKey: peer.PresharedKey, + PersistentKeepalive: NewIntConfigOption(peer.PersistentKeepalive, peer.OvrPersistentKeepalive), + Identifier: peer.Identifier, + Uid: PeerIdentifier(peer.Uid), + AddressStr: NewStringConfigOption(peer.AddressStr, peer.OvrAddressStr), + Dns: NewStringConfigOption(peer.Dns, peer.OvrDns), + Mtu: NewIntConfigOption(peer.Mtu, peer.OvrMtu), + FirewallMark: NewInt32ConfigOption(int32(peer.FirewallMark), peer.OvrFirewallMark), + RoutingTable: NewStringConfigOption(peer.RoutingTable, peer.OvrRoutingTable), + PreUp: NewStringConfigOption(peer.PreUp, peer.OvrPreUp), + PostUp: NewStringConfigOption(peer.PostUp, peer.OvrPostUp), + PreDown: NewStringConfigOption(peer.PreDown, peer.OvrPreDown), + PostDown: NewStringConfigOption(peer.PostDown, peer.OvrPostDown), + + DeactivatedAt: peer.DisabledAt, + CreatedBy: peer.CreatedBy, + UpdatedBy: peer.UpdatedBy, + CreatedAt: peer.CreatedAt, + UpdatedAt: peer.UpdatedAt, + } + } + + return interfaceConfig, peerConfigs, nil +} + +func (d DatabaseBackend) LoadAll() (map[InterfaceConfig][]PeerConfig, error) { + interfaceIdentifiers := []DeviceIdentifier{} // TODO: fill this ?! + + result := make(map[InterfaceConfig][]PeerConfig) + for _, identifier := range interfaceIdentifiers { + iface, peers, err := d.Load(identifier) + if err != nil { + return nil, errors.Wrapf(err, "failed to load data for %s", identifier) + } + result[iface] = peers + } + + return result, nil +} + +// +// --- Models +// + +type dbBaseModel struct { + CreatedBy string + UpdatedBy string + CreatedAt time.Time + UpdatedAt time.Time +} + +type dbInterfaceConfig struct { + dbBaseModel + DisabledAt sql.NullTime + + // WireGuard specific (for the [interface] section of the config file) + + DeviceName string `gorm:"primaryKey"` + PrivateKey string + PublicKey string + ListenPort int + + AddressStr string + Dns string + + Mtu int + FirewallMark int + RoutingTable string + + PreUp string + PostUp string + PreDown string + PostDown string + + SaveConfig bool + + // WG Portal specific + Enabled bool + DisplayName string + Type string + DriverType string + + // Default settings for the peer, used for new peers, those settings will be published to ConfigOption options of + // the peer config + + dbDefaultPeerConfig dbDefaultPeerConfig +} + +func (d dbInterfaceConfig) TableName() string { + return "interface" +} + +type dbDefaultPeerConfig struct { + dbBaseModel + + DeviceName string `gorm:"primaryKey"` // Foreign key + + NetworkStr string // the default subnets from which peers will get their IP addresses, comma seperated + Dns string // the default dns server for the peer + Endpoint string // the default endpoint for the peer + AllowedIPsString string // the default allowed IP string for the peer + Mtu int // the default device MTU + PersistentKeepalive int // the default persistent keep-alive Value + FirewallMark int // default firewall mark + RoutingTable string // the default routing table + + PreUp string // default action that is executed before the device is up + PostUp string // default action that is executed after the device is up + PreDown string // default action that is executed before the device is down + PostDown string // default action that is executed after the device is down +} + +func (d dbDefaultPeerConfig) TableName() string { + return "peer_defaults" +} + +type dbPeerConfig struct { + dbBaseModel + DisabledAt sql.NullTime + + DeviceName string `gorm:"primaryKey"` + Endpoint string + OvrEndpoint bool + AllowedIPsString string + OvrAllowedIPsString bool + ExtraAllowedIPsString string + PrivateKey string + PublicKey string + PresharedKey string + PersistentKeepalive int + OvrPersistentKeepalive bool + + // WG Portal specific + + Identifier string + Uid string `gorm:"primaryKey"` + + // Interface settings for the peer, used to generate the [interface] section in the peer config file + + AddressStr string + OvrAddressStr bool + Dns string + OvrDns bool + Mtu int + OvrMtu bool + FirewallMark int + OvrFirewallMark bool + RoutingTable string + OvrRoutingTable bool + + PreUp string + OvrPreUp bool + PostUp string + OvrPostUp bool + PreDown string + OvrPreDown bool + PostDown string + OvrPostDown bool +} + +func (d dbPeerConfig) TableName() string { + return "peer" +} + +func convertPeer(peer PeerConfig, devName DeviceIdentifier) dbPeerConfig { + return dbPeerConfig{ + DeviceName: string(devName), + Endpoint: peer.Endpoint.GetValue(), + OvrEndpoint: peer.Endpoint.Overridable, + AllowedIPsString: peer.AllowedIPsString.GetValue(), + OvrAllowedIPsString: peer.AllowedIPsString.Overridable, + ExtraAllowedIPsString: peer.ExtraAllowedIPsString, + PrivateKey: peer.KeyPair.PrivateKey, + PublicKey: peer.KeyPair.PublicKey, + PresharedKey: peer.PresharedKey, + PersistentKeepalive: peer.PersistentKeepalive.GetValue(), + OvrPersistentKeepalive: peer.PersistentKeepalive.Overridable, + Identifier: peer.Identifier, + Uid: string(peer.Uid), + AddressStr: peer.AddressStr.GetValue(), + OvrAddressStr: peer.AddressStr.Overridable, + Dns: peer.Dns.GetValue(), + OvrDns: peer.Dns.Overridable, + Mtu: peer.Mtu.GetValue(), + OvrMtu: peer.Mtu.Overridable, + FirewallMark: int(peer.FirewallMark.GetValue()), + OvrFirewallMark: peer.FirewallMark.Overridable, + RoutingTable: peer.RoutingTable.GetValue(), + OvrRoutingTable: peer.RoutingTable.Overridable, + PreUp: peer.PreUp.GetValue(), + OvrPreUp: peer.PreUp.Overridable, + PostUp: peer.PostUp.GetValue(), + OvrPostUp: peer.PostUp.Overridable, + PreDown: peer.PreDown.GetValue(), + OvrPreDown: peer.PreDown.Overridable, + PostDown: peer.PostDown.GetValue(), + OvrPostDown: peer.PostDown.Overridable, + } +} + +func convertInterface(iface InterfaceConfig) (dbInterfaceConfig, dbDefaultPeerConfig) { + cfg := dbInterfaceConfig{ + DeviceName: string(iface.DeviceName), + PrivateKey: iface.KeyPair.PrivateKey, + PublicKey: iface.KeyPair.PublicKey, + ListenPort: iface.ListenPort, + AddressStr: iface.AddressStr, + Dns: iface.Dns, + Mtu: iface.Mtu, + FirewallMark: int(iface.FirewallMark), + RoutingTable: iface.RoutingTable, + PreUp: iface.PreUp, + PostUp: iface.PostUp, + PreDown: iface.PreDown, + PostDown: iface.PostDown, + SaveConfig: iface.SaveConfig, + Enabled: iface.Enabled, + DisplayName: iface.DisplayName, + Type: string(iface.Type), + DriverType: iface.DriverType, + } + peerDefaults := dbDefaultPeerConfig{ + DeviceName: string(iface.DeviceName), + NetworkStr: iface.PeerDefNetworkStr, + Dns: iface.PeerDefDns, + Endpoint: iface.PeerDefEndpoint, + AllowedIPsString: iface.PeerDefAllowedIPsString, + Mtu: iface.PeerDefMtu, + PersistentKeepalive: iface.PeerDefPersistentKeepalive, + FirewallMark: int(iface.PeerDefFirewallMark), + RoutingTable: iface.PeerDefRoutingTable, + PreUp: iface.PeerDefPreUp, + PostUp: iface.PeerDefPostUp, + PreDown: iface.PeerDefPreDown, + PostDown: iface.PeerDefPostDown, + } + + return cfg, peerDefaults +} diff --git a/internal/wireguard/persistant_file.go b/internal/wireguard/persistant_file.go new file mode 100644 index 0000000..f9fcf21 --- /dev/null +++ b/internal/wireguard/persistant_file.go @@ -0,0 +1,29 @@ +package wireguard + +type FileBackend struct { + ConfigurationPath string +} + +func (f FileBackend) SaveInterface(cfg InterfaceConfig, peers []PeerConfig) error { + panic("implement me") +} + +func (f FileBackend) SavePeer(peer PeerConfig, cfg InterfaceConfig) error { + panic("implement me") +} + +func (f FileBackend) DeleteInterface(cfg InterfaceConfig, peers []PeerConfig) error { + panic("implement me") +} + +func (f FileBackend) DeletePeer(peer PeerConfig, cfg InterfaceConfig) error { + panic("implement me") +} + +func (f FileBackend) Load(identifier DeviceIdentifier) (InterfaceConfig, []PeerConfig, error) { + panic("implement me") +} + +func (f FileBackend) LoadAll() (map[InterfaceConfig][]PeerConfig, error) { + panic("implement me") +}