diff --git a/firewalldb/db.go b/firewalldb/db.go new file mode 100644 index 00000000..e0e7f81b --- /dev/null +++ b/firewalldb/db.go @@ -0,0 +1,112 @@ +package firewalldb + +import ( + "encoding/binary" + "fmt" + "os" + "path/filepath" + "time" + + "go.etcd.io/bbolt" +) + +const ( + // DBFilename is the default filename of the rules' database. + DBFilename = "rules.db" + + // dbFilePermission is the default permission the rules' database file + // is created with. + dbFilePermission = 0600 + + // DefaultRulesDBTimeout is the default maximum time we wait for the + // db bbolt database to be opened. If the database is already + // opened by another process, the unique lock cannot be obtained. With + // the timeout we error out after the given time instead of just + // blocking for forever. + DefaultRulesDBTimeout = 5 * time.Second +) + +var ( + // byteOrder is the default byte order we'll use for serialization + // within the database. + byteOrder = binary.BigEndian +) + +// DB is a bolt-backed persistent store. +type DB struct { + *bbolt.DB +} + +// NewDB creates a new bolt database that can be found at the given directory. +func NewDB(dir, fileName string) (*DB, error) { + firstInit := false + path := filepath.Join(dir, fileName) + + // If the database file does not exist yet, create its directory. + if !fileExists(path) { + if err := os.MkdirAll(dir, 0700); err != nil { + return nil, err + } + firstInit = true + } + + db, err := initDB(path, firstInit) + if err != nil { + return nil, err + } + + // Attempt to sync the database's current version with the latest known + // version available. + if err := syncVersions(db); err != nil { + return nil, err + } + + return &DB{DB: db}, nil +} + +// fileExists reports whether the named file or directory exists. +func fileExists(path string) bool { + if _, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + return false + } + } + return true +} + +// initDB initializes all the required top-level buckets for the database. +func initDB(filepath string, firstInit bool) (*bbolt.DB, error) { + db, err := bbolt.Open(filepath, dbFilePermission, &bbolt.Options{ + Timeout: DefaultRulesDBTimeout, + }) + if err == bbolt.ErrTimeout { + return nil, fmt.Errorf("error while trying to open %s: timed "+ + "out after %v when trying to obtain exclusive lock", + filepath, DefaultRulesDBTimeout) + } + if err != nil { + return nil, err + } + + err = db.Update(func(tx *bbolt.Tx) error { + if firstInit { + metadataBucket, err := tx.CreateBucketIfNotExists( + metadataBucketKey, + ) + if err != nil { + return err + } + err = setDBVersion(metadataBucket, latestDBVersion) + if err != nil { + return err + } + } + + return nil + }) + if err != nil { + return nil, err + } + + return db, nil +} diff --git a/firewalldb/log.go b/firewalldb/log.go new file mode 100644 index 00000000..48fbb13a --- /dev/null +++ b/firewalldb/log.go @@ -0,0 +1,25 @@ +package firewalldb + +import ( + "github.com/btcsuite/btclog" + "github.com/lightningnetwork/lnd/build" +) + +const Subsystem = "FWDB" + +// log is a logger that is initialized with no output filters. This +// means the package will not perform any logging by default until the caller +// requests it. +var log btclog.Logger + +// The default amount of logging is none. +func init() { + UseLogger(build.NewSubLogger(Subsystem, nil)) +} + +// UseLogger uses a specified Logger to output package logging info. +// This should be used in preference to SetLogWriter if the caller is also +// using btclog. +func UseLogger(logger btclog.Logger) { + log = logger +} diff --git a/firewalldb/metadata.go b/firewalldb/metadata.go new file mode 100644 index 00000000..ff9438e9 --- /dev/null +++ b/firewalldb/metadata.go @@ -0,0 +1,121 @@ +package firewalldb + +import ( + "errors" + "fmt" + + "go.etcd.io/bbolt" +) + +// migration is a function which takes a prior outdated version of the database +// instance and mutates the key/bucket structure to arrive at a more up-to-date +// version of the database. +type migration func(tx *bbolt.Tx) error + +var ( + // metadataBucketKey stores all the metadata concerning the state of the + // database. + metadataBucketKey = []byte("metadata") + + // dbVersionKey is the key used for storing/retrieving the current + // database version. + dbVersionKey = []byte("version") + + // ErrDBReversion is returned when detecting an attempt to revert to a + // prior database version. + ErrDBReversion = errors.New("cannot revert to prior version") + + // dbVersions is storing all versions of database. If the current + // version of the database doesn't match the latest version this list + // will be used for retrieving all migration function that are need to + // apply to the current db. + dbVersions []migration + + latestDBVersion = uint32(len(dbVersions)) +) + +// getDBVersion retrieves the current database version. +func getDBVersion(bucket *bbolt.Bucket) (uint32, error) { + versionBytes := bucket.Get(dbVersionKey) + if versionBytes == nil { + return 0, errors.New("database version not found") + } + return byteOrder.Uint32(versionBytes), nil +} + +// setDBVersion updates the current database version. +func setDBVersion(bucket *bbolt.Bucket, version uint32) error { + var b [4]byte + byteOrder.PutUint32(b[:], version) + return bucket.Put(dbVersionKey, b[:]) +} + +// getBucket retrieves the bucket with the given key. +func getBucket(tx *bbolt.Tx, key []byte) (*bbolt.Bucket, error) { + bucket := tx.Bucket(key) + if bucket == nil { + return nil, fmt.Errorf("bucket \"%v\" does not exist", + string(key)) + } + return bucket, nil +} + +// syncVersions function is used for safe db version synchronization. It +// applies migration functions to the current database and recovers the +// previous state of db if at least one error/panic appeared during migration. +func syncVersions(db *bbolt.DB) error { + var currentVersion uint32 + err := db.View(func(tx *bbolt.Tx) error { + metadata, err := getBucket(tx, metadataBucketKey) + if err != nil { + return err + } + currentVersion, err = getDBVersion(metadata) + return err + }) + if err != nil { + return err + } + + log.Infof("Checking for schema update: latest_version=%v, "+ + "db_version=%v", latestDBVersion, currentVersion) + + switch { + // If the database reports a higher version that we are aware of, the + // user is probably trying to revert to a prior version of lnd. We fail + // here to prevent reversions and unintended corruption. + case currentVersion > latestDBVersion: + log.Errorf("Refusing to revert from db_version=%d to "+ + "lower version=%d", currentVersion, + latestDBVersion) + + return ErrDBReversion + + // If the current database version matches the latest version number, + // then we don't need to perform any migrations. + case currentVersion == latestDBVersion: + return nil + } + + log.Infof("Performing database schema migration") + + // Otherwise, we execute the migrations serially within a single + // database transaction to ensure the migration is atomic. + return db.Update(func(tx *bbolt.Tx) error { + for v := currentVersion; v < latestDBVersion; v++ { + log.Infof("Applying migration #%v", v+1) + + migration := dbVersions[v] + if err := migration(tx); err != nil { + log.Infof("Unable to apply migration #%v", v+1) + return err + } + } + + metadata, err := getBucket(tx, metadataBucketKey) + if err != nil { + return err + } + return setDBVersion(metadata, latestDBVersion) + }) +} diff --git a/log.go b/log.go index db679a4c..cb8a4cb0 100644 --- a/log.go +++ b/log.go @@ -6,6 +6,7 @@ import ( "github.com/lightninglabs/lightning-node-connect/mailbox" "github.com/lightninglabs/lightning-terminal/accounts" "github.com/lightninglabs/lightning-terminal/firewall" + "github.com/lightninglabs/lightning-terminal/firewalldb" mid "github.com/lightninglabs/lightning-terminal/rpcmiddleware" "github.com/lightninglabs/lightning-terminal/session" "github.com/lightninglabs/loop/loopd" @@ -69,6 +70,9 @@ func SetupLoggers(root *build.RotatingLogWriter, intercept signal.Interceptor) { lnd.AddSubLogger( root, firewall.Subsystem, intercept, firewall.UseLogger, ) + lnd.AddSubLogger( + root, firewalldb.Subsystem, intercept, firewalldb.UseLogger, + ) // Add daemon loggers to lnd's root logger. faraday.SetupLoggers(root, intercept)