checking permissions before accepting websocket

pull/19/head
Niko PLP 1 year ago
parent d4bab0830a
commit cd1f9822a0
  1. 57
      Cargo.lock
  2. 4
      README.md
  3. 2
      ng-sdk-js/README.md
  4. 11
      ng-sdk-js/app-node/package-lock.json
  5. 8
      ngd/src/cli.rs
  6. 54
      ngd/src/main.rs
  7. 4
      ngd/src/types.rs
  8. 8
      p2p-broker/Cargo.toml
  9. 72
      p2p-broker/src/interfaces.rs
  10. 533
      p2p-broker/src/server_ws.rs
  11. 122
      p2p-broker/src/types.rs
  12. 2
      p2p-client-ws/Cargo.toml
  13. 1
      p2p-client-ws/src/remote_ws.rs
  14. 3
      p2p-net/Cargo.toml
  15. 119
      p2p-net/src/broker.rs
  16. 5
      p2p-net/src/connection.rs
  17. 1
      p2p-net/src/errors.rs
  18. 297
      p2p-net/src/types.rs
  19. 39
      p2p-net/src/utils.rs
  20. 4
      p2p-repo/src/lib.rs

57
Cargo.lock generated

@ -359,16 +359,16 @@ dependencies = [
[[package]]
name = "async-tungstenite"
version = "0.17.2"
version = "0.22.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1b71b31561643aa8e7df3effe284fa83ab1a840e52294c5f4bd7bfd8b2becbb"
checksum = "ce01ac37fdc85f10a43c43bc582cbd566720357011578a935761075f898baf58"
dependencies = [
"async-std",
"futures-io",
"futures-util",
"log",
"pin-project-lite",
"tungstenite 0.17.3",
"tungstenite 0.19.0",
]
[[package]]
@ -1105,6 +1105,12 @@ dependencies = [
"syn 2.0.16",
]
[[package]]
name = "data-encoding"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "debug_print"
version = "1.0.0"
@ -2974,10 +2980,10 @@ dependencies = [
"futures",
"getrandom 0.2.9",
"hex",
"once_cell",
"p2p-client-ws",
"p2p-net",
"p2p-repo",
"rust-fsm",
"serde",
"serde_bare",
"serde_bytes",
@ -3017,6 +3023,7 @@ dependencies = [
"async-std",
"async-trait",
"blake3",
"default-net",
"ed25519-dalek",
"futures",
"getrandom 0.2.9",
@ -3769,25 +3776,6 @@ dependencies = [
"walkdir",
]
[[package]]
name = "rust-fsm"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "021d7de715253e45ad24a2fbb0725a0f7f271fd8d3163b130bd65ce2816a860d"
dependencies = [
"rust-fsm-dsl",
]
[[package]]
name = "rust-fsm-dsl"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a66b1273014079e4cf2b04aad1f3a2849e26e9a106f0411be2b1c15c23a791a"
dependencies = [
"quote",
"syn 1.0.109",
]
[[package]]
name = "rustc_version"
version = "0.4.0"
@ -4061,17 +4049,6 @@ dependencies = [
"stable_deref_trait",
]
[[package]]
name = "sha-1"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c"
dependencies = [
"cfg-if",
"cpufeatures",
"digest 0.10.7",
]
[[package]]
name = "sha1"
version = "0.10.5"
@ -4919,9 +4896,9 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
[[package]]
name = "tungstenite"
version = "0.17.3"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0"
checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788"
dependencies = [
"base64 0.13.1",
"byteorder",
@ -4930,7 +4907,7 @@ dependencies = [
"httparse",
"log",
"rand 0.8.5",
"sha-1",
"sha1",
"thiserror",
"url",
"utf-8",
@ -4938,13 +4915,13 @@ dependencies = [
[[package]]
name = "tungstenite"
version = "0.18.0"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788"
checksum = "15fba1a6d6bb030745759a9a2a588bfe8490fc8b4751a277db3a0be1c9ebbf67"
dependencies = [
"base64 0.13.1",
"byteorder",
"bytes",
"data-encoding",
"http",
"httparse",
"log",

@ -71,7 +71,7 @@ cargo run --bin ngd
cargo run --bin ngcli
```
For the web apps, see the [README](ng-app-js/README.md)
For the web apps, see the [README](ng-app/README.md)
### Test
@ -96,7 +96,7 @@ cargo test --package ngcli -- --nocapture
Test WASM websocket
```
cd ng-app-js
cd ng-sdk-js
wasm-pack test --chrome --headless
```

@ -14,7 +14,7 @@ JS/WASM crate containing the SDK of NextGraph
This crate is composed of
- the npm package `ng-app-js` which is the SDK
- the npm package `ng-sdk-js` which is the SDK
- an example of web app using the ESmodule and webpack as bundler `app-web`
- an example of React web app `app-react`
- an example of node-js app `app-node`

@ -9,12 +9,12 @@
"version": "0.1.0",
"license": "(MIT OR Apache-2.0)",
"dependencies": {
"ng-app-node-sdk": "^0.1.0",
"ng-sdk-node": "^0.1.0",
"ws": "^8.13.0"
}
},
"../pkg-node": {
"name": "ng-app-js-sdk",
"name": "ng-sdk-node",
"version": "0.1.0",
"license": "MIT/Apache-2.0"
},
@ -32,10 +32,6 @@
"node": ">=6.14.2"
}
},
"node_modules/ng-app-node-sdk": {
"resolved": "../pkg-node",
"link": true
},
"node_modules/node-gyp-build": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz",
@ -94,9 +90,6 @@
"node-gyp-build": "^4.3.0"
}
},
"ng-app-node-sdk": {
"version": "file:../pkg-node"
},
"node-gyp-build": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz",

@ -43,6 +43,10 @@ pub(crate) struct Cli {
#[arg(short, long, value_name("INTERFACE:PORT"), default_missing_value("default"), num_args(0..=1))]
pub core: Option<String>,
/// When --core is used, this option will allow clients to connect to the public interface too. Otherwise, by default, they cannot.
#[arg(long, requires("core"))]
pub core_with_clients: bool,
/// Quick config to forward all requests to another BROKER. format is "[DOMAIN/IP:PORT]@PEERID". An IPv6 should be encased in square brackets [IPv6] and the whole option should be between double quotes. Port defaults to 80 for IPs and 443 for domains
#[arg(
short,
@ -67,6 +71,10 @@ pub(crate) struct Cli {
)]
pub public: Option<String>,
/// When --public is used, this option will disallow clients to connect to the public interface too. Otherwise, by default, they can. Should be used in combination with a --domain option
#[arg(long, requires("public"), conflicts_with("private"))]
pub public_without_clients: bool,
/// Quick config to listen for clients and core brokers on PRIVATE_INTERFACE, behind a DMZ or port forwarding of a public dynamic IP. PORTs defaults to 80
#[arg(short('y'), long, value_name("PRIVATE_INTERFACE:PORT,PUBLIC_PORT"), default_missing_value("default"), num_args(0..=1), conflicts_with("public"), conflicts_with("core"))]
pub dynamic: Option<String>,

@ -21,6 +21,10 @@ use p2p_broker::server_ws::run_server_v0;
use p2p_broker::types::*;
use p2p_broker::utils::*;
use p2p_net::types::*;
use p2p_net::utils::is_private_ip;
use p2p_net::utils::is_public_ip;
use p2p_net::utils::is_public_ipv4;
use p2p_net::utils::is_public_ipv6;
use p2p_net::utils::{
gen_keys, is_ipv4_global, is_ipv4_private, is_ipv6_global, is_ipv6_private, keys_from_bytes,
Dual25519Keys, Sensitive, U8Array,
@ -501,8 +505,7 @@ async fn main_inner() -> Result<(), ()> {
}
Some(loopback) => {
overlays_config.server = BrokerOverlayPermission::AllRegisteredUser;
let mut listener =
ListenerV0::new_direct(loopback.name, !args.no_ipv6, local_port);
let mut listener = ListenerV0::new_direct(loopback, !args.no_ipv6, local_port);
listener.accept_direct = false;
let res = prepare_accept_forward_for_domain(domain, &args).map_err(|_| {
log_err!("The --domain-peer option has an invalid key. it must be a base64_url encoded serialization of a PrivKey. cannot start")
@ -535,7 +538,7 @@ async fn main_inner() -> Result<(), ()> {
r.ipv6 = !args.no_ipv6;
} else {
listeners.push(ListenerV0::new_direct(
loopback.name,
loopback,
!args.no_ipv6,
args.local.unwrap(),
));
@ -568,12 +571,13 @@ async fn main_inner() -> Result<(), ()> {
}
Some(public) => {
overlays_config.core = BrokerOverlayPermission::AllRegisteredUser;
overlays_config.server = BrokerOverlayPermission::AllRegisteredUser;
listeners.push(ListenerV0::new_direct(
public.name,
!args.no_ipv6,
arg_value.1,
));
let mut listener = ListenerV0::new_direct(public, !args.no_ipv6, arg_value.1);
if args.core_with_clients {
overlays_config.server = BrokerOverlayPermission::AllRegisteredUser;
} else {
listener.refuse_clients = true;
}
listeners.push(listener);
}
}
}
@ -624,7 +628,9 @@ async fn main_inner() -> Result<(), ()> {
}
overlays_config.core = BrokerOverlayPermission::AllRegisteredUser;
overlays_config.server = BrokerOverlayPermission::AllRegisteredUser;
if !args.public_without_clients {
overlays_config.server = BrokerOverlayPermission::AllRegisteredUser;
}
let ipv6 = public_part.0.map(|ipv6| BindAddress {
port: public_part.1 .1,
@ -633,10 +639,13 @@ async fn main_inner() -> Result<(), ()> {
listeners.push(ListenerV0 {
interface_name: private_interface.name,
if_type: private_interface.if_type,
ipv6: public_part.0.is_some(),
interface_refresh: 0,
port: private_part.1,
discoverable: false,
refuse_clients: args.public_without_clients,
serve_app: true,
accept_direct: false,
accept_forward_for: AcceptForwardForV0::PublicStatic((
BindAddress {
@ -700,16 +709,16 @@ async fn main_inner() -> Result<(), ()> {
&& listeners.last().unwrap().port == arg_value.1
{
let r = listeners.last_mut().unwrap();
r.ipv6 = !args.no_ipv6;
if r.accept_forward_for != AcceptForwardForV0::No {
log_err!("The same private interface is already forwarding with a different setting, probably because of a --public option conflicting with a --dynamic option. Changing the port on one of the interfaces can help. cannot start");
return Err(());
}
r.accept_forward_for =
AcceptForwardForV0::PublicDyn((public_port, 60, "".to_string()));
panic!("this should never happen. --dynamic created after a --private");
//r.ipv6 = !args.no_ipv6;
//r.accept_forward_for = AcceptForwardForV0::PublicDyn((public_port, 60, "".to_string()));
} else {
let mut listener =
ListenerV0::new_direct(inter.name, !args.no_ipv6, arg_value.1);
ListenerV0::new_direct(inter, !args.no_ipv6, arg_value.1);
listener.accept_direct = false;
listener.accept_forward_for =
AcceptForwardForV0::PublicDyn((public_port, 60, "".to_string()));
@ -764,15 +773,18 @@ async fn main_inner() -> Result<(), ()> {
&& listeners.last().unwrap().port == arg_value.1
{
let r = listeners.last_mut().unwrap();
r.ipv6 = !args.no_ipv6;
if r.accept_forward_for != AcceptForwardForV0::No {
log_err!("The same private interface is already forwarding with a different setting, probably because of a --public or --dynamic option conflicting with the --domain-private option. Changing the port on one of the interfaces can help. cannot start");
return Err(());
}
r.accept_forward_for = res;
panic!(
"this should never happen. --domain-private created after a --private"
);
//r.ipv6 = !args.no_ipv6;
//r.accept_forward_for = res;
} else {
let mut listener =
ListenerV0::new_direct(inter.name, !args.no_ipv6, arg_value.1);
ListenerV0::new_direct(inter, !args.no_ipv6, arg_value.1);
listener.accept_direct = false;
listener.accept_forward_for = res;
@ -819,11 +831,7 @@ async fn main_inner() -> Result<(), ()> {
r.accept_direct = true;
r.ipv6 = !args.no_ipv6;
} else {
listeners.push(ListenerV0::new_direct(
inter.name,
!args.no_ipv6,
arg_value.1,
));
listeners.push(ListenerV0::new_direct(inter, !args.no_ipv6, arg_value.1));
}
}
}
@ -954,7 +962,7 @@ async fn main_inner() -> Result<(), ()> {
log_info!("PeerId of node: {}", pubkey);
debug_println!("Private key of peer: {}", prix_key_encoded);
match (config.unwrap()) {
match config.unwrap() {
DaemonConfig::V0(v0) => {
run_server_v0(
privkey,

@ -6,7 +6,3 @@
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
use p2p_broker::types::BrokerOverlayConfigV0;
use p2p_broker::types::ListenerV0;
use p2p_repo::types::PrivKey;
use serde::{Deserialize, Serialize};

@ -18,18 +18,18 @@ serde_bare = "0.5.0"
serde_bytes = "0.11.7"
async-std = { version = "1.12.0", features = ["attributes"] }
futures = "0.3.24"
rust-fsm = "0.6.0"
async-channel = "1.7.1"
tempfile = "3"
hex = "0.4.3"
async-trait = "0.1.64"
async-tungstenite = { version = "0.17.2", features = ["async-std-runtime"] }
async-tungstenite = { version = "0.22.2", features = ["async-std-runtime"] }
blake3 = "1.3.1"
default-net = "0.15"
once_cell = "1.17.1"
[target.'cfg(target_arch = "wasm32")'.dependencies.getrandom]
version = "0.2.7"
features = ["js"]
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
getrandom = "0.2.7"
getrandom = "0.2.7"
default-net = "0.15"

@ -8,57 +8,18 @@
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use p2p_net::utils::{is_ipv4_global, is_ipv4_private, is_ipv6_global, is_ipv6_private};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum InterfaceType {
Loopback,
Private,
Public,
Invalid,
}
impl InterfaceType {
pub fn is_ipv4_valid_for_type(&self, ip: &Ipv4Addr) -> bool {
match self {
InterfaceType::Loopback => ip.is_loopback(),
InterfaceType::Public => is_public_ipv4(ip),
// we allow to bind to link-local for IPv4
InterfaceType::Private => is_ipv4_private(ip),
_ => false,
}
}
pub fn is_ipv6_valid_for_type(&self, ip: &Ipv6Addr) -> bool {
match self {
InterfaceType::Loopback => ip.is_loopback(),
InterfaceType::Public => is_public_ipv6(ip),
// we do NOT allow to bind to link-local for IPv6
InterfaceType::Private => is_ipv6_private(ip),
_ => false,
}
}
}
use p2p_net::types::{Interface, InterfaceType};
use p2p_net::utils::{is_ipv4_private, is_public_ipv4};
#[cfg(not(target_arch = "wasm32"))]
pub fn print_ipv4(ip: &default_net::ip::Ipv4Net) -> String {
format!("{}/{}", ip.addr, ip.prefix_len)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn print_ipv6(ip: &default_net::ip::Ipv6Net) -> String {
format!("{}/{}", ip.addr, ip.prefix_len)
}
#[derive(Clone, Debug)]
pub struct Interface {
pub if_type: InterfaceType,
pub name: String,
pub mac_addr: Option<default_net::interface::MacAddr>,
/// List of Ipv4Net for the network interface
pub ipv4: Vec<default_net::ip::Ipv4Net>,
/// List of Ipv6Net for the network interface
pub ipv6: Vec<default_net::ip::Ipv6Net>,
}
pub fn find_first(list: &Vec<Interface>, iftype: InterfaceType) -> Option<Interface> {
for inf in list {
if inf.if_type == iftype {
@ -90,30 +51,7 @@ pub fn find_name(list: &Vec<Interface>, name: &String) -> Option<Interface> {
None
}
pub fn is_public_ipv4(ip: &Ipv4Addr) -> bool {
// TODO, use core::net::Ipv6Addr.is_global when it will be stable
return is_ipv4_global(ip);
}
pub fn is_public_ipv6(ip: &Ipv6Addr) -> bool {
// TODO, use core::net::Ipv6Addr.is_global when it will be stable
return is_ipv6_global(ip);
}
pub fn is_public_ip(ip: &IpAddr) -> bool {
match ip {
IpAddr::V4(v4) => is_public_ipv4(v4),
IpAddr::V6(v6) => is_public_ipv6(v6),
}
}
pub fn is_private_ip(ip: &IpAddr) -> bool {
match ip {
IpAddr::V4(v4) => is_ipv4_private(v4),
IpAddr::V6(v6) => is_ipv6_private(v6),
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn get_interface() -> Vec<Interface> {
let mut res: Vec<Interface> = vec![];
let interfaces = default_net::get_interfaces();

@ -13,20 +13,31 @@
use crate::interfaces::*;
use crate::types::*;
use async_std::io::ReadExt;
use async_std::net::{TcpListener, TcpStream};
use async_std::sync::Mutex;
use async_std::task;
use async_tungstenite::accept_async;
use async_tungstenite::accept_hdr_async;
use async_tungstenite::tungstenite::handshake::server::{
Callback, ErrorResponse, Request, Response,
};
use async_tungstenite::tungstenite::http::header::{CONNECTION, HOST, ORIGIN, UPGRADE};
use async_tungstenite::tungstenite::http::HeaderValue;
use async_tungstenite::tungstenite::http::StatusCode;
use async_tungstenite::tungstenite::protocol::Message;
use futures::{SinkExt, StreamExt};
use once_cell::sync::OnceCell;
use p2p_client_ws::remote_ws::ConnectionWebSocket;
use p2p_net::broker::*;
use p2p_net::connection::IAccept;
use p2p_net::types::IP;
use p2p_net::utils::Sensitive;
use p2p_net::types::*;
use p2p_net::utils::is_private_ip;
use p2p_net::utils::is_public_ip;
use p2p_net::utils::{get_domain_without_port, Sensitive, U8Array};
use p2p_repo::log::*;
use p2p_repo::types::{PrivKey, PubKey};
use p2p_repo::utils::generate_keypair;
use std::collections::HashMap;
use std::collections::HashSet;
use std::fs;
use std::net::SocketAddr;
@ -39,22 +50,382 @@ use stores_lmdb::kcv_store::LmdbKCVStore;
use stores_lmdb::repo_store::LmdbRepoStore;
use tempfile::Builder;
pub async fn accept(tcp: TcpStream, peer_priv_key: Sensitive<[u8; 32]>, peer_pub_key: PubKey) {
let sock_addr = tcp.peer_addr().unwrap();
let ip = sock_addr.ip();
let mut ws = accept_async(tcp).await.unwrap();
static LISTENERS_INFO: OnceCell<(HashMap<String, ListenerInfo>, HashMap<BindAddress, String>)> =
OnceCell::new();
struct SecurityCallback {
remote_bind_address: BindAddress,
local_bind_address: BindAddress,
}
let cws = ConnectionWebSocket {};
let base = cws.accept(peer_priv_key, peer_pub_key, ws).await.unwrap();
impl SecurityCallback {
fn new(remote_bind_address: BindAddress, local_bind_address: BindAddress) -> Self {
Self {
remote_bind_address,
local_bind_address,
}
}
}
fn make_error(code: StatusCode) -> ErrorResponse {
Response::builder().status(code).body(None).unwrap()
}
fn check_origin_is_url(
origin: Option<&HeaderValue>,
domains: Vec<String>,
) -> Result<(), ErrorResponse> {
match origin {
None => Ok(()),
Some(val) => {
for domain in domains {
if val.to_str().unwrap().starts_with(domain.as_str()) {
return Ok(());
}
}
Err(make_error(StatusCode::FORBIDDEN))
}
}
}
fn check_xff_is_public_or_private(
xff: Option<&HeaderValue>,
none_is_ok: bool,
public: bool,
) -> Result<(), ErrorResponse> {
match xff {
None => {
if none_is_ok {
Ok(())
} else {
Err(make_error(StatusCode::FORBIDDEN))
}
}
Some(val) => {
let ip_str = val
.to_str()
.map_err(|_| make_error(StatusCode::FORBIDDEN))?;
let ip: IpAddr = ip_str
.parse()
.map_err(|_| make_error(StatusCode::FORBIDDEN))?;
if public && !is_public_ip(&ip) || !public && !is_private_ip(&ip) {
Err(make_error(StatusCode::FORBIDDEN))
} else {
Ok(())
}
}
}
}
fn check_no_xff(xff: Option<&HeaderValue>) -> Result<(), ErrorResponse> {
match xff {
None => Ok(()),
Some(_) => Err(make_error(StatusCode::FORBIDDEN)),
}
}
fn check_host(host: Option<&HeaderValue>, hosts: Vec<String>) -> Result<(), ErrorResponse> {
match host {
None => Err(make_error(StatusCode::FORBIDDEN)),
Some(val) => {
for hos in hosts {
if val.to_str().unwrap().starts_with(&hos) {
return Ok(());
}
}
Err(make_error(StatusCode::FORBIDDEN))
}
}
}
fn check_host_in_addrs(
host: Option<&HeaderValue>,
addrs: &Vec<BindAddress>,
) -> Result<(), ErrorResponse> {
match host {
None => Err(make_error(StatusCode::FORBIDDEN)),
Some(val) => {
for ba in addrs {
if val.to_str().unwrap().starts_with(&ba.ip.to_string()) {
return Ok(());
}
}
Err(make_error(StatusCode::FORBIDDEN))
}
}
}
fn prepare_domain_url_and_host(
accept_forward_for: &AcceptForwardForV0,
) -> (Vec<String>, Vec<String>) {
let domain_str = accept_forward_for.get_domain();
let url = ["https://", domain_str].concat();
let hosts_str = vec![domain_str.to_string()];
let urls_str = vec![url];
(hosts_str, urls_str)
}
fn prepare_urls_from_private_addrs(addrs: &Vec<BindAddress>, port: u16) -> Vec<String> {
let port_str = if port != 80 {
[":", &port.to_string()].concat()
} else {
"".to_string()
};
let mut res: Vec<String> = vec![];
for addr in addrs {
let url = ["http://", &addr.ip.to_string(), &port_str].concat();
res.push(url);
}
res
}
fn upgrade_ws_or_serve_app(
upgrade: Option<&HeaderValue>,
remote: IP,
serve_app: bool,
response: Response,
) -> Result<Response, ErrorResponse> {
if upgrade.is_some()
&& upgrade
.unwrap()
.to_str()
.unwrap()
.split(|c| c == ' ' || c == ',')
.any(|p| p.eq_ignore_ascii_case("Upgrade"))
{
return Ok(response);
}
if serve_app && (remote.is_private() || remote.is_loopback()) {
return Err(make_error(StatusCode::OK));
}
Err(make_error(StatusCode::FORBIDDEN))
}
const LOCAL_HOSTS: [&str; 3] = ["localhost", "127.0.0.1", "::1"];
const LOCAL_URLS: [&str; 3] = ["http://localhost", "http://127.0.0.1", "http://::1"];
const APP_NG_ONE_URL: &str = "https://app.nextgraph.one";
impl Callback for SecurityCallback {
fn on_request(self, request: &Request, response: Response) -> Result<Response, ErrorResponse> {
let local_urls = LOCAL_URLS
.to_vec()
.iter()
.map(ToString::to_string)
.collect();
let local_hosts = LOCAL_HOSTS
.to_vec()
.iter()
.map(ToString::to_string)
.collect();
let (listeners, bind_addresses) = LISTENERS_INFO.get().ok_or(
Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(None)
.unwrap(),
)?;
// check that the remote address is allowed to connect on the listener
let listener_id = bind_addresses
.get(&self.local_bind_address)
.ok_or(make_error(StatusCode::FORBIDDEN))?;
let listener = listeners
.get(listener_id)
.ok_or(make_error(StatusCode::FORBIDDEN))?;
let xff = request.headers().get("X-Forwarded-For");
let upgrade = request.headers().get(CONNECTION);
let host = request.headers().get(HOST);
let origin = request.headers().get(ORIGIN);
let remote = self.remote_bind_address.ip;
let xff = request.headers().get("X-Forwarded-For");
log_debug!(
"upgrade:{:?} origin:{:?} host:{:?} xff:{:?} remote:{:?} local:{:?}",
upgrade,
origin,
host,
xff,
remote,
self.local_bind_address
);
match listener.config.if_type {
InterfaceType::Public => {
if !remote.is_public() {
return Err(make_error(StatusCode::FORBIDDEN));
}
check_no_xff(xff)?;
let mut urls_str = vec![];
if !listener.config.refuse_clients {
urls_str.push(APP_NG_ONE_URL.to_string());
}
check_origin_is_url(origin, urls_str)?;
check_host_in_addrs(host, &listener.addrs)?;
log_debug!(
"accepted core with refuse_clients {}",
listener.config.refuse_clients
);
return Ok(response);
}
InterfaceType::Loopback => {
if !remote.is_loopback() {
return Err(make_error(StatusCode::FORBIDDEN));
}
if listener.config.accept_forward_for.is_public_domain() {
let (mut hosts_str, mut urls_str) =
prepare_domain_url_and_host(&listener.config.accept_forward_for);
if listener.config.accept_direct {
hosts_str = [hosts_str, local_hosts].concat();
// TODO local_urls might need a trailing :port, but it is ok for now as we do starts_with
urls_str = [urls_str, local_urls].concat();
}
check_origin_is_url(origin, urls_str)?;
check_host(host, hosts_str)?;
check_xff_is_public_or_private(xff, listener.config.accept_direct, true)?;
log_debug!(
"accepted loopback PUBLIC_DOMAIN with direct {}",
listener.config.accept_direct
);
return upgrade_ws_or_serve_app(
upgrade,
remote,
listener.config.serve_app,
response,
);
} else if listener.config.accept_forward_for.is_private_domain() {
let (hosts_str, urls_str) =
prepare_domain_url_and_host(&listener.config.accept_forward_for);
check_origin_is_url(origin, urls_str)?;
check_host(host, hosts_str)?;
check_xff_is_public_or_private(xff, false, false)?;
log_debug!("accepted loopback PRIVATE_DOMAIN");
return Ok(response);
} else if listener.config.accept_forward_for == AcceptForwardForV0::No {
check_host(host, local_hosts)?;
check_no_xff(xff)?;
// TODO local_urls might need a trailing :port, but it is ok for now as we do starts_with
check_origin_is_url(origin, local_urls)?;
log_debug!("accepted loopback DIRECT");
return Ok(response);
}
}
InterfaceType::Private => {
if listener.config.accept_forward_for.is_public_static()
|| listener.config.accept_forward_for.is_public_dyn()
{
if !listener.config.accept_direct && !remote.is_public()
|| listener.config.accept_direct
&& !remote.is_private()
&& !remote.is_public()
{
return Err(make_error(StatusCode::FORBIDDEN));
}
check_no_xff(xff)?;
//TODO FIXME get remote_peer_id from ConnectionBase (once it is available)
let (priv_key, pub_key) = generate_keypair();
let remote_peer_id = pub_key;
let mut addrs = listener
.config
.accept_forward_for
.get_public_bind_addresses();
let mut urls_str = vec![];
if !listener.config.refuse_clients {
urls_str.push(APP_NG_ONE_URL.to_string());
}
if listener.config.accept_direct {
addrs.extend(&listener.addrs);
urls_str = [
urls_str,
prepare_urls_from_private_addrs(&listener.addrs, listener.config.port),
]
.concat();
}
check_origin_is_url(origin, urls_str)?;
check_host_in_addrs(host, &addrs)?;
log_debug!("accepted private PUBLIC_STATIC or PUBLIC_DYN with direct {} with refuse_clients {}",listener.config.accept_direct, listener.config.refuse_clients);
return Ok(response);
} else if listener.config.accept_forward_for.is_public_domain() {
if !remote.is_private() {
return Err(make_error(StatusCode::FORBIDDEN));
}
check_xff_is_public_or_private(xff, listener.config.accept_direct, true)?;
let (mut hosts_str, mut urls_str) =
prepare_domain_url_and_host(&listener.config.accept_forward_for);
if listener.config.accept_direct {
for addr in listener.addrs.iter() {
let str = addr.ip.to_string();
hosts_str.push(str);
}
urls_str = [
urls_str,
prepare_urls_from_private_addrs(&listener.addrs, listener.config.port),
]
.concat();
}
check_origin_is_url(origin, urls_str)?;
check_host(host, hosts_str)?;
log_debug!(
"accepted private PUBLIC_DOMAIN with direct {}",
listener.config.accept_direct
);
return Ok(response);
} else if listener.config.accept_forward_for == AcceptForwardForV0::No {
if !remote.is_private() {
return Err(make_error(StatusCode::FORBIDDEN));
}
check_no_xff(xff)?;
check_host_in_addrs(host, &listener.addrs)?;
check_origin_is_url(
origin,
prepare_urls_from_private_addrs(&listener.addrs, listener.config.port),
)?;
log_debug!("accepted private DIRECT");
return Ok(response);
}
}
_ => {}
}
Err(make_error(StatusCode::FORBIDDEN))
}
}
pub async fn accept(tcp: TcpStream, peer_priv_key: Sensitive<[u8; 32]>) {
let remote_addr = tcp.peer_addr().unwrap();
let remote_bind_address: BindAddress = (&remote_addr).into();
let local_addr = tcp.local_addr().unwrap();
let local_bind_address: BindAddress = (&local_addr).into();
let ws = accept_hdr_async(
tcp,
SecurityCallback::new(remote_bind_address, local_bind_address),
)
.await;
if ws.is_err() {
log_debug!("websocket rejected {:?}", ws.err());
//let mut buffer = Vec::new();
//tcp.read_to_end(&mut buffer).await;
//log_debug!("{:?}", buffer);
return;
}
log_debug!("websocket accepted");
let cws = ConnectionWebSocket {};
let base = cws.accept(peer_priv_key, ws.unwrap()).await.unwrap();
let res = BROKER
.write()
.await
.accept(base, IP::try_from(&ip).unwrap(), None, remote_peer_id)
.accept(base, remote_bind_address, local_bind_address)
.await;
}
@ -77,11 +448,15 @@ pub async fn run_server_accept_one(
let tcp = connections.next().await.unwrap()?;
accept(tcp, peer_priv_key, peer_pub_key).await;
{
BROKER.write().await.set_my_peer_id(peer_pub_key);
}
accept(tcp, peer_priv_key).await;
Ok(())
}
use p2p_net::utils::U8Array;
pub async fn run_server_v0(
peer_priv_key: Sensitive<[u8; 32]>,
peer_pub_key: PubKey,
@ -91,26 +466,29 @@ pub async fn run_server_v0(
) -> Result<(), ()> {
// check config
let mut should_run = false;
for overlay_conf in config.overlays_configs {
if overlay_conf.core != BrokerOverlayPermission::Nobody
|| overlay_conf.server != BrokerOverlayPermission::Nobody
{
should_run = true;
break;
let mut run_core = false;
let mut run_server = false;
for overlay_conf in config.overlays_configs.iter() {
if overlay_conf.core != BrokerOverlayPermission::Nobody {
run_core = true;
}
if overlay_conf.server != BrokerOverlayPermission::Nobody {
run_server = true;
}
}
if !should_run {
if !run_core && !run_server {
log_err!("There isn't any overlay_config that should run as core or server. Check your config. cannot start");
return Err(());
}
let listeners: HashSet<String> = HashSet::new();
if run_core && !run_server {
log_warn!("There isn't any overlay_config that should run as server. This is a misconfiguration as a core server that cannot receive client connections is useless");
}
let mut listeners: HashSet<String> = HashSet::new();
for listener in &config.listeners {
let mut id = listener.interface_name.clone();
id.push('@');
id.push_str(&listener.port.to_string());
if listeners.contains(&id) {
let id: String = listener.to_string();
if !listeners.insert(id.clone()) {
log_err!(
"The listener {} is defined twice. Check your config file. cannot start",
id
@ -130,8 +508,23 @@ pub async fn run_server_v0(
let store = LmdbKCVStore::open(&path, master_key);
let interfaces = get_interface();
let mut listener_infos: HashMap<String, ListenerInfo> = HashMap::new();
let mut listeners_addrs: Vec<(Vec<SocketAddr>, String)> = vec![];
let mut listeners: Vec<TcpListener> = vec![];
let mut accept_clients = false;
// TODO: check that there is only one PublicDyn or one PublicStatic or one Core
// Preparing the listeners addrs and infos
for listener in config.listeners {
if !listener.accept_direct && listener.accept_forward_for == AcceptForwardForV0::No {
log_warn!(
"The interface {} does not accept direct connections nor is configured to forward. it is therefor disabled",
listener.interface_name
);
continue;
}
match find_name(&interfaces, &listener.interface_name) {
None => {
log_err!(
@ -141,7 +534,7 @@ pub async fn run_server_v0(
return Err(());
}
Some(interface) => {
let mut ips: Vec<SocketAddr> = interface
let mut addrs: Vec<SocketAddr> = interface
.ipv4
.iter()
.filter_map(|ip| {
@ -152,7 +545,7 @@ pub async fn run_server_v0(
}
})
.collect();
if ips.len() == 0 {
if addrs.len() == 0 {
log_err!(
"The interface {} does not have any IPv4 address. cannot start",
listener.interface_name
@ -171,28 +564,72 @@ pub async fn run_server_v0(
}
})
.collect();
ips.append(&mut ipv6s);
addrs.append(&mut ipv6s);
}
let ips_string = ips
.iter()
.map(|ip| ip.to_string())
.collect::<Vec<String>>()
.join(", ");
let listener = TcpListener::bind(ips.as_slice()).await.map_err(|e| {
log_err!(
"cannot bind to {} with addresses {} : {}",
interface.name,
ips_string,
e.to_string()
)
})?;
log_info!("Listening on {} {}", interface.name, ips_string);
listeners.push(listener);
if !listener.refuse_clients {
accept_clients = true;
}
if listener.refuse_clients && listener.accept_forward_for.is_public_domain() {
log_warn!(
"You have disabled accepting connections from clients on {}. This is unusual as --domain and --domain-private listeners are meant to answer to clients only. This will activate the relay_websocket on this listener. Is it really intended?",
listener.interface_name
);
}
let listener_id: String = listener.to_string();
let listener_info = ListenerInfo {
config: listener,
addrs: addrs.iter().map(|addr| addr.into()).collect(),
};
listener_infos.insert(listener_id, listener_info);
listeners_addrs.push((addrs, interface.name));
}
}
}
if listeners_addrs.len() == 0 {
log_err!("No listener configured. cannot start",);
return Err(());
}
if !accept_clients {
log_warn!("There isn't any listener that accept clients. This is a misconfiguration as a core server that cannot receive client connections is useless");
}
// saving the infos in the broker. This needs to happen before we start listening, as new incoming connections can happen anytime after that.
// and we need those infos for permission checking.
{
let mut broker = BROKER.write().await;
broker.set_my_peer_id(peer_pub_key);
LISTENERS_INFO
.set(broker.set_listeners(listener_infos))
.unwrap();
broker.set_overlays_configs(config.overlays_configs);
}
// Actually starting the listeners
for addrs in listeners_addrs {
let addrs_string = addrs
.0
.iter()
.map(SocketAddr::to_string)
.collect::<Vec<String>>()
.join(", ");
let tcp_listener = TcpListener::bind(addrs.0.as_slice()).await.map_err(|e| {
log_err!(
"cannot bind to {} with addresses {} : {}",
addrs.1,
addrs_string,
e.to_string()
)
})?;
log_info!("Listening on {} {}", addrs.1, addrs_string);
listeners.push(tcp_listener);
}
// select on all listeners
let mut incoming = futures::stream::select_all(
listeners
@ -200,12 +637,14 @@ pub async fn run_server_v0(
.map(TcpListener::into_incoming)
.map(Box::pin),
);
// Iterate over all incoming connections
// TODO : select on the shutdown stream too
while let Some(tcp) = incoming.next().await {
accept(
tcp.unwrap(),
Sensitive::<[u8; 32]>::from_slice(peer_priv_key.deref()),
peer_pub_key,
)
.await;
}

@ -6,130 +6,10 @@
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
use p2p_net::types::{BindAddress, BrokerServerV0, OverlayId, UserId, IP};
use p2p_net::types::{BrokerOverlayConfigV0, ListenerV0};
use p2p_repo::types::PrivKey;
use serde::{Deserialize, Serialize};
/// AcceptForwardForV0 type
/// allow answers to connection requests originating from a client behind a reverse proxy
/// Format of last param in the tuple is a list of comma separated hosts or CIDR subnetworks IPv4 and/or IPv6 addresses accepted as X-Forwarded-For
/// Empty string means all addresses are accepted
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum AcceptForwardForV0 {
/// X-Forwarded-For not allowed
No,
/// X-Forwarded-For accepted only for clients with private LAN addresses. First param is the domain of the proxy server
Private((String, String)),
/// X-Forwarded-For accepted only for clients with public addresses. First param is the domain of the proxy server
/// domain can take an option port (trailing `:port`)
PublicDomain((String, String)),
/// X-Forwarded-For accepted only for clients with public addresses. First param is the domain of the proxy server
/// domain can take an option port (trailing `:port`)
/// second param is the privKey of the PeerId of the proxy server, useful when the proxy server is load balancing to several daemons
/// that should all use the same PeerId to answer requests
PublicDomainPeer((String, PrivKey, String)),
/// accepts only clients with public addresses that arrive on a LAN address binding. This is used for DMZ and port forwarding configs
/// first param is the port, second param in tuple is the interval for periodic probe of the external IP
PublicDyn((u16, u32, String)),
/// accepts only clients with public addresses that arrive on a LAN address binding. This is used for DMZ and port forwarding configs
/// First param is the IPv4 bind address of the reverse NAT server (DMZ, port forwarding)
/// Second param is ab optional IPv6 bind address of the reverse NAT server (DMZ, port forwarding)
PublicStatic((BindAddress, Option<BindAddress>, String)),
}
/// DaemonConfig Listener Version 0
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ListenerV0 {
/// local interface name to bind to
/// names of interfaces can be retrieved with the --list-interfaces option
pub interface_name: String,
/// optional number of seconds for an interval of periodic refresh
/// of the actual IP(s) of the interface. Used for dynamic IP interfaces (DHCP)
pub interface_refresh: u32,
// if to bind to the ipv6 address of the interface
pub ipv6: bool,
/// local port to listen on
pub port: u16,
// will answer a probe coming from private LAN and if is_private, with its own peerId, so that guests on the network will be able to connect.
pub discoverable: bool,
/// Answers to connection requests originating from a direct client, without X-Forwarded-For headers
/// Can be used in combination with a accept_forward_for config, when a local daemon is behind a proxy, and also serves as broker for local apps/webbrowsers
pub accept_direct: bool,
/// X-Forwarded-For config. only valid if IP/interface is localhost or private
pub accept_forward_for: AcceptForwardForV0,
// impl fn is_private()
// returns false if public IP in interface, or if PublicDyn, PublicStatic
// if the ip is local or private, and the forwarding is not PublicDyn nor PublicStatic, (if is_private) then the app is served on HTTP get of /
// an interface with no accept_forward_for and no accept_direct, is de facto, disabled
}
impl ListenerV0 {
pub fn new_direct(name: String, ipv6: bool, port: u16) -> Self {
Self {
interface_name: name,
interface_refresh: 0,
ipv6,
port,
discoverable: false,
accept_direct: true,
accept_forward_for: AcceptForwardForV0::No,
}
}
}
/// Broker Overlay Permission
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum BrokerOverlayPermission {
Nobody,
Anybody,
AllRegisteredUser,
UsersList(Vec<UserId>),
}
/// Broker Overlay Config
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct BrokerOverlayConfigV0 {
// list of overlays this config applies to. empty array means applying to all
pub overlays: Vec<OverlayId>,
// Who can ask to join an overlay on the core
pub core: BrokerOverlayPermission,
// Who can connect as a client to this server
pub server: BrokerOverlayPermission,
// if core == Nobody and server == Nobody then the listeners will not be started
// are ExtRequest allowed on the server? this requires the core to be ON.
pub allow_read: bool,
/// an empty list means to forward to the peer known for each overlay.
/// forward and core are mutually exclusive. forward becomes the default when core is disabled (set to Nobody).
/// core always takes precedence.
pub forward: Vec<BrokerServerV0>,
}
impl BrokerOverlayConfigV0 {
pub fn new() -> Self {
BrokerOverlayConfigV0 {
overlays: vec![],
core: BrokerOverlayPermission::Nobody,
server: BrokerOverlayPermission::Nobody</