Gate insecure capabilities behind footgun feature

This commit is contained in:
Brandon Vandegrift 2025-05-08 21:05:28 -04:00
parent 29eeebd9c1
commit 7a2a8ae4cb
15 changed files with 143 additions and 77 deletions

View file

@ -8,6 +8,10 @@
- Renamed config structs to better describe their purpose, and remove "Inner" from a struct that's being exposed via the API. ([!402](https://gitlab.com/veilid/veilid/-/merge_requests/402)) - Renamed config structs to better describe their purpose, and remove "Inner" from a struct that's being exposed via the API. ([!402](https://gitlab.com/veilid/veilid/-/merge_requests/402))
- `VeilidConfig` -> `VeilidStartupOptions` - `VeilidConfig` -> `VeilidStartupOptions`
- `VeilidConfigInner` -> `VeilidConfig` - `VeilidConfigInner` -> `VeilidConfig`
- Gate insecure capabilities behind the new `footgun` feature flag ([#394](https://gitlab.com/veilid/veilid/-/issues/394)), which is disabled by default. ([!400](https://gitlab.com/veilid/veilid/-/merge_requests/400))
- Calling `app_call` or `app_message` with a `NodeId` target will throw an error. Use a `PrivateRoute` target instead.
- Creating an `Unsafe` routing context will throw and error. Use `Safe` routing context instead.
- Any AppCall or AppMessage sent from a direct NodeId will not be received.
- veilid-core: - veilid-core:
- **Security** Signed bootstrap v1 added which closes #293: https://gitlab.com/veilid/veilid/-/issues/293 - **Security** Signed bootstrap v1 added which closes #293: https://gitlab.com/veilid/veilid/-/issues/293

View file

@ -62,6 +62,10 @@ virtual-network-server = ["veilid-tools/virtual-network-server"]
# GeoIP # GeoIP
geolocation = ["maxminddb", "reqwest"] geolocation = ["maxminddb", "reqwest"]
# Features that go against "Natural Security" concepts
# https://gitlab.com/veilid/veilid/-/issues/420
footgun = []
### DEPENDENCIES ### DEPENDENCIES
[dependencies] [dependencies]

View file

@ -73,6 +73,11 @@ impl VeilidCoreContext {
veilid_log!(registry info "Veilid API starting up"); veilid_log!(registry info "Veilid API starting up");
veilid_log!(registry info "Version: {}", veilid_version_string()); veilid_log!(registry info "Version: {}", veilid_version_string());
veilid_log!(registry info "Features: {:?}", veilid_features()); veilid_log!(registry info "Features: {:?}", veilid_features());
#[cfg(feature = "footgun")]
{
veilid_log!(registry warn
"Footgun feature is enabled. This disables sender privacy protections and should be avoided in production.");
}
// Register all components // Register all components
registry.register(ProtectedStore::new); registry.register(ProtectedStore::new);

View file

@ -118,6 +118,15 @@ impl RPCProcessor {
.as_ref() .as_ref()
.map(|nr| nr.node_ids().get(crypto_kind).unwrap()); .map(|nr| nr.node_ids().get(crypto_kind).unwrap());
#[cfg(not(feature = "footgun"))]
{
if sender.is_some() {
return Ok(NetworkResult::invalid_message(
"Direct NodeId senders are not allowed for AppCall when footgun is disabled",
));
}
}
// Register a waiter for this app call // Register a waiter for this app call
let handle = self.waiting_app_call_table.add_op_waiter(op_id, ()); let handle = self.waiting_app_call_table.add_op_waiter(op_id, ());

View file

@ -81,6 +81,15 @@ impl RPCProcessor {
.as_ref() .as_ref()
.map(|nr| nr.node_ids().get(crypto_kind).unwrap()); .map(|nr| nr.node_ids().get(crypto_kind).unwrap());
#[cfg(not(feature = "footgun"))]
{
if sender.is_some() {
return Ok(NetworkResult::invalid_message(
"Direct NodeId senders are not allowed for AppMessage when footgun is disabled",
));
}
}
// Pass the message up through the update callback // Pass the message up through the update callback
let message = app_message.destructure(); let message = app_message.destructure();
(self.update_callback())(VeilidUpdate::AppMessage(Box::new(VeilidAppMessage::new( (self.update_callback())(VeilidUpdate::AppMessage(Box::new(VeilidAppMessage::new(

View file

@ -11,55 +11,35 @@ lazy_static! {
} }
pub async fn test_get_dht_value_unopened(api: VeilidAPI) { pub async fn test_get_dht_value_unopened(api: VeilidAPI) {
let rc = api let rc = api.routing_context().unwrap();
.routing_context()
.unwrap()
.with_safety(SafetySelection::Unsafe(Sequencing::EnsureOrdered))
.unwrap();
let result = rc.get_dht_value(*BOGUS_KEY, 0, false).await; let result = rc.get_dht_value(*BOGUS_KEY, 0, false).await;
assert_err!(result); assert_err!(result);
} }
pub async fn test_open_dht_record_nonexistent_no_writer(api: VeilidAPI) { pub async fn test_open_dht_record_nonexistent_no_writer(api: VeilidAPI) {
let rc = api let rc = api.routing_context().unwrap();
.routing_context()
.unwrap()
.with_safety(SafetySelection::Unsafe(Sequencing::EnsureOrdered))
.unwrap();
let result = rc.get_dht_value(*BOGUS_KEY, 0, false).await; let result = rc.get_dht_value(*BOGUS_KEY, 0, false).await;
assert_err!(result); assert_err!(result);
} }
pub async fn test_close_dht_record_nonexistent(api: VeilidAPI) { pub async fn test_close_dht_record_nonexistent(api: VeilidAPI) {
let rc = api let rc = api.routing_context().unwrap();
.routing_context()
.unwrap()
.with_safety(SafetySelection::Unsafe(Sequencing::EnsureOrdered))
.unwrap();
let result = rc.close_dht_record(*BOGUS_KEY).await; let result = rc.close_dht_record(*BOGUS_KEY).await;
assert_err!(result); assert_err!(result);
} }
pub async fn test_delete_dht_record_nonexistent(api: VeilidAPI) { pub async fn test_delete_dht_record_nonexistent(api: VeilidAPI) {
let rc = api let rc = api.routing_context().unwrap();
.routing_context()
.unwrap()
.with_safety(SafetySelection::Unsafe(Sequencing::EnsureOrdered))
.unwrap();
let result = rc.delete_dht_record(*BOGUS_KEY).await; let result = rc.delete_dht_record(*BOGUS_KEY).await;
assert_err!(result); assert_err!(result);
} }
pub async fn test_create_delete_dht_record_simple(api: VeilidAPI) { pub async fn test_create_delete_dht_record_simple(api: VeilidAPI) {
let rc = api let rc = api.routing_context().unwrap();
.routing_context()
.unwrap()
.with_safety(SafetySelection::Unsafe(Sequencing::EnsureOrdered))
.unwrap();
let rec = rc let rec = rc
.create_dht_record(DHTSchema::dflt(1).unwrap(), None, Some(CRYPTO_KIND_VLD0)) .create_dht_record(DHTSchema::dflt(1).unwrap(), None, Some(CRYPTO_KIND_VLD0))
@ -72,11 +52,7 @@ pub async fn test_create_delete_dht_record_simple(api: VeilidAPI) {
} }
pub async fn test_create_dht_record_with_owner(api: VeilidAPI) { pub async fn test_create_dht_record_with_owner(api: VeilidAPI) {
let rc = api let rc = api.routing_context().unwrap();
.routing_context()
.unwrap()
.with_safety(SafetySelection::Unsafe(Sequencing::EnsureOrdered))
.unwrap();
let crypto = api.crypto().unwrap(); let crypto = api.crypto().unwrap();
let cs = crypto.get(CRYPTO_KIND_VLD0).unwrap(); let cs = crypto.get(CRYPTO_KIND_VLD0).unwrap();
@ -99,11 +75,7 @@ pub async fn test_create_dht_record_with_owner(api: VeilidAPI) {
} }
pub async fn test_get_dht_record_key(api: VeilidAPI) { pub async fn test_get_dht_record_key(api: VeilidAPI) {
let rc = api let rc = api.routing_context().unwrap();
.routing_context()
.unwrap()
.with_safety(SafetySelection::Unsafe(Sequencing::EnsureOrdered))
.unwrap();
let crypto = api.crypto().unwrap(); let crypto = api.crypto().unwrap();
let cs = crypto.get(CRYPTO_KIND_VLD0).unwrap(); let cs = crypto.get(CRYPTO_KIND_VLD0).unwrap();
@ -130,11 +102,7 @@ pub async fn test_get_dht_record_key(api: VeilidAPI) {
} }
pub async fn test_get_dht_value_nonexistent(api: VeilidAPI) { pub async fn test_get_dht_value_nonexistent(api: VeilidAPI) {
let rc = api let rc = api.routing_context().unwrap();
.routing_context()
.unwrap()
.with_safety(SafetySelection::Unsafe(Sequencing::EnsureOrdered))
.unwrap();
let rec = rc let rec = rc
.create_dht_record(DHTSchema::dflt(1).unwrap(), None, Some(CRYPTO_KIND_VLD0)) .create_dht_record(DHTSchema::dflt(1).unwrap(), None, Some(CRYPTO_KIND_VLD0))
@ -149,11 +117,7 @@ pub async fn test_get_dht_value_nonexistent(api: VeilidAPI) {
} }
pub async fn test_set_get_dht_value(api: VeilidAPI) { pub async fn test_set_get_dht_value(api: VeilidAPI) {
let rc = api let rc = api.routing_context().unwrap();
.routing_context()
.unwrap()
.with_safety(SafetySelection::Unsafe(Sequencing::EnsureOrdered))
.unwrap();
let rec = rc let rec = rc
.create_dht_record(DHTSchema::dflt(2).unwrap(), None, Some(CRYPTO_KIND_VLD0)) .create_dht_record(DHTSchema::dflt(2).unwrap(), None, Some(CRYPTO_KIND_VLD0))
@ -201,11 +165,7 @@ pub async fn test_set_get_dht_value(api: VeilidAPI) {
} }
pub async fn test_open_writer_dht_value(api: VeilidAPI) { pub async fn test_open_writer_dht_value(api: VeilidAPI) {
let rc = api let rc = api.routing_context().unwrap();
.routing_context()
.unwrap()
.with_safety(SafetySelection::Unsafe(Sequencing::EnsureOrdered))
.unwrap();
let rec = rc let rec = rc
.create_dht_record(DHTSchema::dflt(2).unwrap(), None, Some(CRYPTO_KIND_VLD0)) .create_dht_record(DHTSchema::dflt(2).unwrap(), None, Some(CRYPTO_KIND_VLD0))

View file

@ -101,6 +101,15 @@ impl RoutingContext {
veilid_log!(self debug veilid_log!(self debug
"RoutingContext::with_safety(self: {:?}, safety_selection: {:?})", self, safety_selection); "RoutingContext::with_safety(self: {:?}, safety_selection: {:?})", self, safety_selection);
if let SafetySelection::Unsafe(_) = safety_selection {
#[cfg(not(feature = "footgun"))]
{
return Err(VeilidAPIError::generic(
"Unsafe routing mode is not allowed without the 'footgun' feature enabled",
));
}
}
Ok(Self { Ok(Self {
api: self.api.clone(), api: self.api.clone(),
unlocked_inner: Arc::new(RoutingContextUnlockedInner { safety_selection }), unlocked_inner: Arc::new(RoutingContextUnlockedInner { safety_selection }),
@ -161,19 +170,12 @@ impl RoutingContext {
.map_err(VeilidAPIError::invalid_target) .map_err(VeilidAPIError::invalid_target)
} }
////////////////////////////////////////////////////////////////
// App-level Messaging
/// App-level bidirectional call that expects a response to be returned.
///
/// Veilid apps may use this for arbitrary message passing.
///
/// * `target` - can be either a direct node id or a private route.
/// * `message` - an arbitrary message blob of up to 32768 bytes.
///
/// Returns an answer blob of up to 32768 bytes.
#[instrument(target = "veilid_api", level = "debug", fields(__VEILID_LOG_KEY = self.log_key()), ret, err)] #[instrument(target = "veilid_api", level = "debug", fields(__VEILID_LOG_KEY = self.log_key()), ret, err)]
pub async fn app_call(&self, target: Target, message: Vec<u8>) -> VeilidAPIResult<Vec<u8>> { async fn internal_app_call(
&self,
target: Target,
message: Vec<u8>,
) -> VeilidAPIResult<Vec<u8>> {
veilid_log!(self debug veilid_log!(self debug
"RoutingContext::app_call(self: {:?}, target: {:?}, message: {:?})", self, target, message); "RoutingContext::app_call(self: {:?}, target: {:?}, message: {:?})", self, target, message);
@ -200,14 +202,45 @@ impl RoutingContext {
Ok(answer.answer) Ok(answer.answer)
} }
/// App-level unidirectional message that does not expect any value to be returned. #[cfg(feature = "footgun")]
////////////////////////////////////////////////////////////////
// App-level Messaging
/// App-level bidirectional call that expects a response to be returned.
/// ///
/// Veilid apps may use this for arbitrary message passing. /// Veilid apps may use this for arbitrary message passing.
/// ///
/// * `target` - can be either a direct node id or a private route. /// * `target` - can be either a direct node id or a private route.
/// * `message` - an arbitrary message blob of up to 32768 bytes. /// * `message` - an arbitrary message blob of up to 32768 bytes.
///
/// Returns an answer blob of up to 32768 bytes.
pub async fn app_call(&self, target: Target, message: Vec<u8>) -> VeilidAPIResult<Vec<u8>> {
self.internal_app_call(target, message).await
}
#[cfg(not(feature = "footgun"))]
////////////////////////////////////////////////////////////////
// App-level Messaging
/// App-level bidirectional call that expects a response to be returned.
///
/// Veilid apps may use this for arbitrary message passing.
///
/// * `target` - a private route.
/// * `message` - an arbitrary message blob of up to 32768 bytes.
///
/// Returns an answer blob of up to 32768 bytes.
pub async fn app_call(&self, target: Target, message: Vec<u8>) -> VeilidAPIResult<Vec<u8>> {
match target {
Target::PrivateRoute(_) => self.internal_app_call(target, message).await,
Target::NodeId(_) => Err(VeilidAPIError::invalid_target(
"Only PrivateRoute targets are allowed without the footgun feature",
)),
}
}
#[instrument(target = "veilid_api", level = "debug", fields(__VEILID_LOG_KEY = self.log_key()), ret, err)] #[instrument(target = "veilid_api", level = "debug", fields(__VEILID_LOG_KEY = self.log_key()), ret, err)]
pub async fn app_message(&self, target: Target, message: Vec<u8>) -> VeilidAPIResult<()> { async fn internal_app_message(&self, target: Target, message: Vec<u8>) -> VeilidAPIResult<()> {
veilid_log!(self debug veilid_log!(self debug
"RoutingContext::app_message(self: {:?}, target: {:?}, message: {:?})", self, target, message); "RoutingContext::app_message(self: {:?}, target: {:?}, message: {:?})", self, target, message);
@ -233,6 +266,33 @@ impl RoutingContext {
Ok(()) Ok(())
} }
#[cfg(feature = "footgun")]
/// App-level unidirectional message that does not expect any value to be returned.
///
/// Veilid apps may use this for arbitrary message passing.
///
/// * `target` - can be either a direct node id or a private route.
/// * `message` - an arbitrary message blob of up to 32768 bytes.
pub async fn app_message(&self, target: Target, message: Vec<u8>) -> VeilidAPIResult<()> {
self.internal_app_message(target, message).await
}
#[cfg(not(feature = "footgun"))]
/// App-level unidirectional message that does not expect any value to be returned.
///
/// Veilid apps may use this for arbitrary message passing.
///
/// * `target` - a private route.
/// * `message` - an arbitrary message blob of up to 32768 bytes.
pub async fn app_message(&self, target: Target, message: Vec<u8>) -> VeilidAPIResult<()> {
match target {
Target::PrivateRoute(_) => self.internal_app_message(target, message).await,
Target::NodeId(_) => Err(VeilidAPIError::invalid_target(
"Only PrivateRoute targets are allowed without the footgun feature",
)),
}
}
/////////////////////////////////// ///////////////////////////////////
// DHT Records // DHT Records

View file

@ -319,7 +319,7 @@ Future<void> testWatchDHTValues(Stream<VeilidUpdate> updateStream) async {
// as the watch's target // as the watch's target
final rcSet = await Veilid.instance.routingContext(); final rcSet = await Veilid.instance.routingContext();
final rcWatch = await Veilid.instance.unsafeRoutingContext(); final rcWatch = await Veilid.instance.safeRoutingContext();
try { try {
// Make a DHT record // Make a DHT record
var rec = await rcWatch.createDHTRecord(const DHTSchema.dflt(oCnt: 10)); var rec = await rcWatch.createDHTRecord(const DHTSchema.dflt(oCnt: 10));

View file

@ -32,6 +32,7 @@ rt-tokio = [
"opentelemetry/rt-tokio", "opentelemetry/rt-tokio",
] ]
debug-load = ["dep:ctor", "dep:libc-print", "dep:android_log-sys", "dep:oslog"] debug-load = ["dep:ctor", "dep:libc-print", "dep:android_log-sys", "dep:oslog"]
footgun = ["veilid-core/footgun"]
[dependencies] [dependencies]
veilid-core = { path = "../../veilid-core", default-features = false } veilid-core = { path = "../../veilid-core", default-features = false }

View file

@ -448,11 +448,11 @@ async def test_watch_many_dht_values():
# make routing contexts # make routing contexts
# unsafe version for debugging # unsafe version for debugging
rc0 = await (await api0.new_routing_context()).with_safety(SafetySelection.unsafe()) # rc0 = await (await api0.new_routing_context()).with_safety(SafetySelection.unsafe())
rc1 = await (await api1.new_routing_context()).with_safety(SafetySelection.unsafe()) # rc1 = await (await api1.new_routing_context()).with_safety(SafetySelection.unsafe())
# safe default version # safe default version
# rc0 = await api0.new_routing_context() rc0 = await api0.new_routing_context()
# rc1 = await api1.new_routing_context() rc1 = await api1.new_routing_context()
async with rc0, rc1: async with rc0, rc1:

View file

@ -34,9 +34,15 @@ async def test_routing_contexts(api_connection: veilid.VeilidAPI):
) )
await rc.release() await rc.release()
rc = await (await api_connection.new_routing_context()).with_safety(
# Test that creating an Unsafe routing context throws an error
# as mentioned in the CHANGELOG (footgun feature disabled by default)
rc = await api_connection.new_routing_context()
with pytest.raises(Exception):
rc = await rc.with_safety(
veilid.SafetySelection.unsafe(veilid.Sequencing.PREFER_ORDERED) veilid.SafetySelection.unsafe(veilid.Sequencing.PREFER_ORDERED)
) )
await rc.release() await rc.release()

View file

@ -18,6 +18,7 @@ path = "src/main.rs"
[features] [features]
default = ["rt-tokio", "veilid-core/default", "otlp-tonic"] default = ["rt-tokio", "veilid-core/default", "otlp-tonic"]
default-async-std = ["rt-async-std", "veilid-core/default-async-std"] default-async-std = ["rt-async-std", "veilid-core/default-async-std"]
footgun = ["veilid-core/footgun"]
virtual-network = [ virtual-network = [
"veilid-core/virtual-network", "veilid-core/virtual-network",

View file

@ -18,6 +18,7 @@ path = "src/lib.rs"
[features] [features]
default = ["veilid-core/default-wasm"] default = ["veilid-core/default-wasm"]
crypto-test = ["veilid-core/crypto-test"] crypto-test = ["veilid-core/crypto-test"]
footgun = ["veilid-core/footgun"]
[dependencies] [dependencies]
veilid-core = { version = "0.4.4", path = "../veilid-core", default-features = false } veilid-core = { version = "0.4.4", path = "../veilid-core", default-features = false }

View file

@ -23,7 +23,7 @@ describe('VeilidRoutingContext', () => {
// } // }
}, veilidCoreStartupConfig); }, veilidCoreStartupConfig);
await veilidClient.attach(); await veilidClient.attach();
await asyncCallWithTimeout(waitForPublicAttachment(), 10000); await asyncCallWithTimeout(waitForPublicAttachment(), 30_000);
//console.log("---Started Up---"); //console.log("---Started Up---");
}); });
@ -65,15 +65,21 @@ describe('VeilidRoutingContext', () => {
VeilidRoutingContext.create().withSequencing('EnsureOrdered'); VeilidRoutingContext.create().withSequencing('EnsureOrdered');
expect(routingContext instanceof VeilidRoutingContext).toBe(true); expect(routingContext instanceof VeilidRoutingContext).toBe(true);
}); });
it('should error if unsafe is used', async () => {
expect(() => {
VeilidRoutingContext.create().withSafety({
Unsafe: 'EnsureOrdered',
});
}).toThrow();
});
}); });
describe('operations', () => { describe('operations', () => {
let routingContext: VeilidRoutingContext; let routingContext: VeilidRoutingContext;
before('create routing context', () => { before('create routing context', () => {
routingContext = VeilidRoutingContext.create().withSafety({ routingContext = VeilidRoutingContext.create();
Unsafe: 'EnsureOrdered',
});
}); });
describe('createDhtRecord', () => { describe('createDhtRecord', () => {

View file

@ -75,7 +75,7 @@ describe('veilidClient', function () {
}); });
it('should call debug command', async function () { it('should call debug command', async function () {
const response = await veilidClient.debug('txtrecord'); const response = await veilidClient.debug('help');
expect(response).toBeDefined(); expect(response).toBeDefined();
expect(response.length).toBeGreaterThan(0); expect(response.length).toBeGreaterThan(0);
}); });