-
-
Notifications
You must be signed in to change notification settings - Fork 5
Keyset locking #139
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Keyset locking #139
Changes from all commits
f8c1453
3621409
88c45bd
2ee5528
54fd380
df5ef0e
97e218b
3583c32
19c2ff1
9328445
5d946be
174f31e
2129fc2
688aada
7977bf7
75ce06f
559f00e
ba81d62
018527f
6fad119
df48644
1f8f259
5bf2f3f
64b9ce9
1d6b2f2
6a1e4cb
f50b657
4f11b90
e8dd8e2
c19673f
2c18fac
913ff60
c2acca2
dc5a619
96c94f1
dd78a25
3e6f79e
7d2cece
c62a097
67b99a1
2f7c2f5
be7e53e
cbd7a7b
8f31bfc
2f14623
a4e9c65
1184e35
fb17d93
ce21283
63c3e7f
bbc1551
38b3b53
f00078a
60da418
5e52e61
07d5bfe
c53687a
ef60307
535f572
c84e21c
c21a9db
0a13d74
48d1eb2
4044fe5
ac75810
4f6f762
7accc03
ceca925
7d37971
dd24009
ce572cd
d20b476
8bfef3b
7337a3a
953cd96
212a8ff
84cca6a
9d072a5
570220a
9301b12
10d2fa6
5976442
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,15 +44,17 @@ use domain_kmip as kmip; | |
| use domain_kmip::dep::kmip::client::pool::SyncConnPool; | ||
| #[cfg(feature = "kmip")] | ||
| use domain_kmip::KeyUrl; | ||
| use fs2::FileExt; | ||
| use futures::future::join_all; | ||
| use jiff::{Span, SpanRelativeTo}; | ||
| use same_file::Handle; | ||
| use serde::{Deserialize, Serialize}; | ||
| use std::cmp::max; | ||
| use std::collections::{BTreeMap, HashMap, HashSet}; | ||
| use std::convert::From; | ||
| use std::ffi::OsStr; | ||
| use std::fmt::{Debug, Display, Formatter}; | ||
| use std::fs::{create_dir_all, remove_file, File}; | ||
| use std::fs::{create_dir_all, remove_file, rename, File}; | ||
| use std::io::{self, Write}; | ||
| use std::net::{IpAddr, SocketAddr}; | ||
| use std::path::{absolute, Path, PathBuf}; | ||
|
|
@@ -73,6 +75,9 @@ use super::kmip::{format_key_label, kmip_command, KmipCommands, KmipState}; | |
| /// with the key tags of existing keys. | ||
| const MAX_KEY_TAG_TRIES: u8 = 10; | ||
|
|
||
| /// Number of times to try locking a file. | ||
| const MAX_FILE_LOCK_TRIES: u8 = 10; | ||
|
|
||
| /// Wait this amount before retrying for network errors, DNS errors, etc. | ||
| const DEFAULT_WAIT: Duration = Duration::from_secs(10 * 60); | ||
|
|
||
|
|
@@ -629,6 +634,9 @@ struct WorkSpace { | |
| /// Whether the command to update DS records has to be executed. | ||
| run_update_ds_command: bool, | ||
|
|
||
| /// Store the locked config file to avoid accidental unlocking. | ||
| _locked_config_file: Option<File>, | ||
|
|
||
| #[cfg(feature = "kmip")] | ||
| /// The current set of KMIP server pools. | ||
| pools: HashMap<String, SyncConnPool>, | ||
|
|
@@ -711,32 +719,26 @@ impl Keyset { | |
| ) | ||
| })?; | ||
|
|
||
| let json = serde_json::to_string_pretty(&kss).expect("should not fail"); | ||
| let mut file = File::create(&state_file) | ||
| .map_err(|e| format!("unable to create file {}: {e}", state_file.display()))?; | ||
| write!(file, "{json}") | ||
| .map_err(|e| format!("unable to write to file {}: {e}", state_file.display()))?; | ||
| let ws = WorkSpace { | ||
| config: ksc, | ||
| state: kss, | ||
| config_changed: false, | ||
| state_changed: false, | ||
| run_update_ds_command: false, | ||
| _locked_config_file: None, | ||
| #[cfg(feature = "kmip")] | ||
| pools: HashMap::new(), | ||
| }; | ||
|
|
||
| ws.write_state()?; | ||
| ws.write_config(&self.keyset_conf)?; | ||
|
|
||
| let json = serde_json::to_string_pretty(&ksc).expect("should not fail"); | ||
| let mut file = File::create(&self.keyset_conf).map_err(|e| { | ||
| format!("unable to create file {}: {e}", self.keyset_conf.display()) | ||
| })?; | ||
| write!(file, "{json}").map_err(|e| { | ||
| format!( | ||
| "unable to write to file {}: {e}", | ||
| self.keyset_conf.display() | ||
| ) | ||
| })?; | ||
| return Ok(()); | ||
| } | ||
|
|
||
| let file = File::open(self.keyset_conf.clone()).map_err(|e| { | ||
| format!( | ||
| "unable to open config file {}: {e}", | ||
| self.keyset_conf.display() | ||
| ) | ||
| })?; | ||
| let ksc: KeySetConfig = serde_json::from_reader(file) | ||
| let config_file = file_with_write_lock(&self.keyset_conf)?; | ||
|
|
||
| let ksc: KeySetConfig = serde_json::from_reader(&config_file) | ||
| .map_err(|e| format!("error loading {:?}: {e}\n", self.keyset_conf))?; | ||
| let file = File::open(ksc.state_file.clone()).map_err(|e| { | ||
| format!( | ||
|
|
@@ -753,6 +755,7 @@ impl Keyset { | |
| config_changed: false, | ||
| state_changed: false, | ||
| run_update_ds_command: false, | ||
| _locked_config_file: Some(config_file), | ||
| #[cfg(feature = "kmip")] | ||
| pools: HashMap::new(), | ||
| }; | ||
|
|
@@ -1556,31 +1559,10 @@ impl Keyset { | |
| ws.state_changed = true; | ||
| } | ||
| if ws.config_changed { | ||
| let json = serde_json::to_string_pretty(&ws.config).expect("should not fail"); | ||
| let mut file = File::create(&self.keyset_conf).map_err(|e| { | ||
| format!("unable to create file {}: {e}", self.keyset_conf.display()) | ||
| })?; | ||
| write!(file, "{json}").map_err(|e| { | ||
| format!( | ||
| "unable to write to file {}: {e}", | ||
| self.keyset_conf.display() | ||
| ) | ||
| })?; | ||
| ws.write_config(&self.keyset_conf)?; | ||
| } | ||
| if ws.state_changed { | ||
| let json = serde_json::to_string_pretty(&ws.state).expect("should not fail"); | ||
| let mut file = File::create(&ws.config.state_file).map_err(|e| { | ||
| format!( | ||
| "unable to create file {}: {e}", | ||
| ws.config.state_file.display() | ||
| ) | ||
| })?; | ||
| write!(file, "{json}").map_err(|e| { | ||
| format!( | ||
| "unable to write to file {}: {e}", | ||
| ws.config.state_file.display() | ||
| ) | ||
| })?; | ||
| ws.write_state()?; | ||
| } | ||
|
|
||
| // Now check if we need to run the update_ds_command. Make sure that | ||
|
|
@@ -3760,6 +3742,43 @@ impl WorkSpace { | |
| let new_algs = HashSet::from([self.config.algorithm.to_generate_params().algorithm()]); | ||
| curr_algs != new_algs | ||
| } | ||
|
|
||
| /// Write config to a file. | ||
| fn write_config(&self, keyset_conf: &PathBuf) -> Result<(), Error> { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This and
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
| let json = serde_json::to_string_pretty(&self.config).expect("should not fail"); | ||
| Self::write_to_new_and_rename(&json, keyset_conf) | ||
| } | ||
|
|
||
| /// Write state to a file. | ||
| fn write_state(&self) -> Result<(), Error> { | ||
| let json = serde_json::to_string_pretty(&self.state).expect("should not fail"); | ||
| Self::write_to_new_and_rename(&json, &self.config.state_file) | ||
| } | ||
|
|
||
| /// First write to a new filename and then rename to make sure that | ||
| /// changes are atomic. | ||
| fn write_to_new_and_rename(json: &str, filename: &PathBuf) -> Result<(), Error> { | ||
| let mut filename_new = filename.clone(); | ||
| // It would be nice to use add_extension here, but it is only in | ||
| // Rust 1.91.0 and above. Use strings instead. | ||
| // if !state_file_new.add_extension("new") { | ||
| // return Err(format!("unable to add extension 'new' to {}", | ||
| // ws.config.state_file.display()).into()); | ||
| // } | ||
| filename_new.as_mut_os_string().push(".new"); | ||
| let mut file = File::create(&filename_new) | ||
| .map_err(|e| format!("unable to create file {}: {e}", filename_new.display()))?; | ||
| write!(file, "{json}") | ||
| .map_err(|e| format!("unable to write to file {}: {e}", filename_new.display()))?; | ||
| rename(&filename_new, filename).map_err(|e| { | ||
| format!( | ||
| "unable to rename {} to {}: {e}", | ||
| filename_new.display(), | ||
| filename.display() | ||
| ) | ||
| })?; | ||
| Ok(()) | ||
| } | ||
| } | ||
|
|
||
| /// Create CDS and CDNSKEY RRsets. | ||
|
|
@@ -5658,6 +5677,46 @@ fn show_automatic_roll_state( | |
| } | ||
| } | ||
|
|
||
| /// Open filename, get an exclusive lock and return the open file. | ||
| /// | ||
| /// Assume changes are saved by creating a new file and renaming. After | ||
| /// locking the file, the function has to check if the locked file is the | ||
| /// same as the current file under that name. | ||
| fn file_with_write_lock(filename: &PathBuf) -> Result<File, Error> { | ||
| // The config file is updated by writing to a new file and then renaming. | ||
| // We might have locked the old file. Check. Try a number of times and | ||
| // then give up. Lock contention is expected to be low. | ||
| for _try in 0..MAX_FILE_LOCK_TRIES { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it okay to retry lock acquisition with no delay between attempts?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will lead to the thundering herd problem. But concurrent access to keyset for a single domain is expected to be very low. We need to do something if we ever fail to get a lock within MAX_FILE_LOCK_TRIES. |
||
| let file = File::open(filename) | ||
| .map_err(|e| format!("unable to open file {}: {e}", filename.display()))?; | ||
|
|
||
| file.lock_exclusive() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. According to the documentation this call will block if the file is currently locked, is that desirable or would it be better to call
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For this PR the goal is to block. Maybe the UI needs to be improved in the future. |
||
| .map_err(|e| format!("unable to lock {}: {e}", filename.display()))?; | ||
|
|
||
| let file_clone = file | ||
| .try_clone() | ||
| .map_err(|e| format!("unable to clone locked file {}: {e}", filename.display()))?; | ||
| let locked_file_handle = Handle::from_file(file_clone).map_err(|e| { | ||
| format!( | ||
| "Unable to get handle from locked file {}: {e}", | ||
| filename.display() | ||
| ) | ||
| })?; | ||
| let current_file_handle = Handle::from_path(filename) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You open a file by path, lock it and get its handle, then get another handle for the same path and compare the two handles. Why? What is this handle comparison achieving/intended to do?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is explained by the comment at the top of the function. Maybe it needs more text. The idea is that because the config is saved by creating a new file and then renaming that file, it is possible to endup with a lock on the old config file that no longer exists. So check that we have locked to current config file.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You only call Also, when this test fails you call If this function were called while a second concurrent invocation of
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After a rename, any processes that run keyset and that were trying to lock the config file now try to lock the wrong file. So they need to start again. The logic of keyset is that it gets a lock on the config file, and then saves the state file if it changed and saves the config file if it changed. There will not be a second call to lock the config file in a single run of keyset. |
||
| .map_err(|e| format!("Unable to get handle from file {}: {e}", filename.display()))?; | ||
|
|
||
| if locked_file_handle != current_file_handle { | ||
| continue; | ||
| } | ||
| return Ok(file); | ||
| } | ||
| Err(format!( | ||
| "unable to lock {} after {MAX_FILE_LOCK_TRIES} tries", | ||
| filename.display() | ||
| ) | ||
| .into()) | ||
| } | ||
|
|
||
| /// Helper function for serde. | ||
| /// | ||
| /// Return the default autoremove delay. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering if this is a good choice of dependency. What were your reasons for selecting it?
I ask because:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll leave it at fs2 for now. If fs2 gets less popular or picks up security issues then we can switch. The interface of fd-lock looks scary.