mirror of
https://github.com/Twigpine/node.git
synced 2026-11-08 17:55:53 -10:00
Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66fb3ffc95 | ||
|
|
5a93f2435c | ||
|
|
6dde36b02d | ||
|
|
3a296485fb | ||
|
|
f29235dc31 | ||
|
|
1948d3d31b | ||
|
|
77331acbdb | ||
|
|
806c262fae | ||
|
|
31b1176467 | ||
|
|
2a69a0bdfc | ||
|
|
f7fcd5edf5 | ||
|
|
532ffc7202 | ||
|
|
f75b0f4afa | ||
|
|
15d81c530c |
No files matched your search
@@ -7,6 +7,17 @@
|
||||
# Generate with: gl identity new
|
||||
GITLAWB_KEY=/data/keys/identity.pem
|
||||
|
||||
# Path to the node's persistent libp2p identity key file. Must include a
|
||||
# directory; the node refuses to start on a bare filename, because it will not
|
||||
# keep its p2p identity key in the working directory. On Unix it is created
|
||||
# 0600 inside a 0700 directory, and a loose key directory is tightened to 0700
|
||||
# on start; on other platforms no permissions are enforced. If the node logs
|
||||
# that it tightened a loose key directory, treat the key that was sitting there
|
||||
# as possibly exposed: delete it so a fresh identity is generated on the next
|
||||
# start. Keep it on a persistent volume so the PeerId survives redeploys.
|
||||
# Default: ~/.gitlawb/p2p.key
|
||||
#GITLAWB_P2P_KEY=/data/keys/p2p.key
|
||||
|
||||
# Publicly reachable URL of this node (used in peer announcements)
|
||||
GITLAWB_PUBLIC_URL=https://your-node.example.com
|
||||
|
||||
|
||||
Generated
+1
@@ -3484,6 +3484,7 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
"unicode-normalization",
|
||||
"uuid",
|
||||
"zeroize",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ WORKDIR /data
|
||||
|
||||
ENV GITLAWB_REPOS_DIR=/data/repos \
|
||||
GITLAWB_KEY=/data/keys/identity.pem \
|
||||
GITLAWB_P2P_KEY=/data/keys/p2p.key \
|
||||
GITLAWB_HOST=0.0.0.0 \
|
||||
GITLAWB_PORT=7545 \
|
||||
GITLAWB_P2P_PORT=7546
|
||||
|
||||
@@ -339,6 +339,7 @@ Important node settings:
|
||||
| `GITLAWB_P2P_PORT` | libp2p QUIC/UDP port. Use `0` to disable. |
|
||||
| `GITLAWB_BOOTSTRAP_PEERS` | Comma-separated HTTP peer URLs. |
|
||||
| `GITLAWB_P2P_BOOTSTRAP` | Comma-separated libp2p multiaddrs. |
|
||||
| `GITLAWB_P2P_KEY` | Path to the node's persistent libp2p identity key file, which fixes the PeerId across restarts. Must include a directory, such as `/data/keys/p2p.key` or `./keys/p2p.key`; the node refuses to start on a bare filename, because it will not keep its p2p identity key in the working directory. On Unix the file is created `0600` inside a `0700` directory, and a loose key directory is tightened to `0700` on start; on other platforms no permissions are enforced. Default `~/.gitlawb/p2p.key`; point it at a persistent volume when running in a container. |
|
||||
| `GITLAWB_BOOTSTRAP_DISABLE_SEEDS` | Disable embedded seed peers for isolated dev/test networks. |
|
||||
| `GITLAWB_REQUIRE_SIGNED_PEER_WRITES` | Require signed peer announce/sync writes. |
|
||||
| `GITLAWB_AUTO_SYNC` | Enable automatic sync from known peers. |
|
||||
@@ -362,6 +363,27 @@ Important node settings:
|
||||
|
||||
Production note: change the default Postgres password before exposing a node publicly.
|
||||
|
||||
### Upgrading: the PeerId rotates once
|
||||
|
||||
This node's libp2p identity is now a keypair generated on first start and kept
|
||||
at `GITLAWB_P2P_KEY`, rather than one derived from the node DID. Every node
|
||||
therefore gets a new PeerId once, on the first start after upgrading, and keeps
|
||||
it from then on as long as that key file survives (put it on a persistent volume
|
||||
in a container).
|
||||
|
||||
Two things to check before upgrading:
|
||||
|
||||
- Any `GITLAWB_P2P_BOOTSTRAP` multiaddr that pins a peer's old PeerId with a
|
||||
`/p2p/<PeerId>` suffix stops matching once that peer upgrades. Update the
|
||||
suffix, or drop it and let identify supply the current one. Addresses without
|
||||
the suffix keep working untouched.
|
||||
- `GITLAWB_P2P_KEY` must name a directory. A bare filename is refused at
|
||||
startup, since the node will not keep its identity key in the working
|
||||
directory.
|
||||
|
||||
Peers found over `GITLAWB_BOOTSTRAP_PEERS` and the embedded seed list are
|
||||
unaffected, since those are HTTP URLs and carry no PeerId.
|
||||
|
||||
---
|
||||
|
||||
## Optional node staking
|
||||
|
||||
@@ -33,6 +33,7 @@ sqlx = { version = "0.8", features = ["postgres", "runtime-tokio-rustls", "chron
|
||||
clap = { version = "4", features = ["derive", "env"] }
|
||||
bytes = "1"
|
||||
libc = "0.2"
|
||||
zeroize = "1"
|
||||
cid = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use clap::Parser;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Upper bound on `git_service_timeout_secs` and `ipfs_request_budget_secs`, in seconds
|
||||
/// (100 years).
|
||||
@@ -104,6 +104,10 @@ pub struct Config {
|
||||
#[arg(long, env = "GITLAWB_P2P_PORT", default_value_t = 7546)]
|
||||
pub p2p_port: u16,
|
||||
|
||||
/// Path to the persistent libp2p identity key
|
||||
#[arg(long, env = "GITLAWB_P2P_KEY", default_value = "~/.gitlawb/p2p.key")]
|
||||
pub p2p_key_path: String,
|
||||
|
||||
/// libp2p bootstrap multiaddrs (comma-separated)
|
||||
/// Example: /ip4/1.2.3.4/udp/7546/quic-v1/p2p/12D3KooW...
|
||||
#[arg(long, env = "GITLAWB_P2P_BOOTSTRAP", value_delimiter = ',')]
|
||||
@@ -556,6 +560,16 @@ impl Config {
|
||||
PathBuf::from(&self.key_path)
|
||||
}
|
||||
|
||||
/// Resolve ~ in p2p_key_path
|
||||
pub fn resolved_p2p_key_path(&self) -> PathBuf {
|
||||
if self.p2p_key_path.starts_with("~/") {
|
||||
if let Some(home) = dirs_next::home_dir() {
|
||||
return home.join(&self.p2p_key_path[2..]);
|
||||
}
|
||||
}
|
||||
PathBuf::from(&self.p2p_key_path)
|
||||
}
|
||||
|
||||
/// DB connections reserved for everything other than held write-locks: auth
|
||||
/// lookups, visibility-rule reads, the post-receive tail's own DB writes, and
|
||||
/// admin tooling. A write pins one pooled connection for its whole duration, so
|
||||
@@ -583,6 +597,48 @@ impl Config {
|
||||
floor
|
||||
));
|
||||
}
|
||||
|
||||
// A p2p key path naming no directory puts the node's private key in
|
||||
// whatever directory the process was started from. The node cannot
|
||||
// protect that: `ensure_key_dir` would have to chmod a directory the
|
||||
// operator never nominated as a key directory, and a directory it
|
||||
// cannot secure is one where any local user with write access can
|
||||
// replace the key and choose the node's libp2p identity. Refuse it here,
|
||||
// where the denial actually stops the process, rather than in the p2p
|
||||
// start path, where main.rs logs the error and keeps serving with a
|
||||
// green /health.
|
||||
//
|
||||
// Decided lexically on the resolved path: `canonicalize` would fail on a
|
||||
// parent that does not exist yet (the shipped `~/.gitlawb` default, and
|
||||
// every container's first boot), and comparing against the process
|
||||
// working directory would reject `/data/p2p.key` under the image's
|
||||
// WORKDIR, an absolute directory the operator did name.
|
||||
// `resolved_p2p_key_path` expands a leading `~/` only when a home
|
||||
// directory is resolvable, and otherwise hands back the literal string.
|
||||
// That would leave the shipped default naming a directory called `~`
|
||||
// relative to wherever the process started, which is a real directory
|
||||
// the node would create and chmod, and whose location moves with the
|
||||
// working directory. It passes the check below because `~` is an
|
||||
// ordinary path component, so it has to be caught separately.
|
||||
let p2p_key_path = self.resolved_p2p_key_path();
|
||||
if self.p2p_key_path.starts_with("~/") && p2p_key_path == Path::new(&self.p2p_key_path) {
|
||||
return Err(format!(
|
||||
"GITLAWB_P2P_KEY ({}) starts with `~/` but no home directory could be resolved, \
|
||||
so it would name a literal `~` directory relative to the working directory. \
|
||||
Set an absolute path such as /data/keys/p2p.key.",
|
||||
self.p2p_key_path
|
||||
));
|
||||
}
|
||||
if crate::p2p::names_no_usable_directory(&p2p_key_path) {
|
||||
return Err(format!(
|
||||
"GITLAWB_P2P_KEY ({}) must include a directory that does not walk back through \
|
||||
`..`, such as ./keys/p2p.key or /data/keys/p2p.key: the node will not store its \
|
||||
p2p identity key in the working directory, where the directory holding it \
|
||||
cannot be secured.",
|
||||
self.p2p_key_path
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -973,4 +1029,81 @@ mod tests {
|
||||
"db_max_connections at the floor (pushes + headroom) must validate"
|
||||
);
|
||||
}
|
||||
|
||||
fn config_with_p2p_key(path: &str) -> Config {
|
||||
Config::parse_from(["gitlawb-node", "--p2p-key-path", path])
|
||||
}
|
||||
|
||||
/// A p2p key path that names no directory component would put the node's
|
||||
/// private key in whatever directory the process happens to be started from,
|
||||
/// which `ensure_key_dir` cannot protect without tightening a directory the
|
||||
/// operator never nominated. Reject it at boot instead.
|
||||
#[test]
|
||||
fn p2p_key_path_without_a_directory_component_is_rejected() {
|
||||
for path in [
|
||||
// No directory component at all.
|
||||
"p2p.key",
|
||||
"./p2p.key",
|
||||
"././p2p.key",
|
||||
"p2p.key/",
|
||||
"",
|
||||
// Looks like it names a directory and does not: each of these
|
||||
// resolves back to the working directory or above it, so accepting
|
||||
// them would defeat the check and chmod an unnominated directory.
|
||||
"a/../p2p.key",
|
||||
"./keys/../p2p.key",
|
||||
"../p2p.key",
|
||||
// Absolute too: the lexical parent is what gets chmodded, so these
|
||||
// would tighten /data and / rather than the named directory.
|
||||
"/data/keys/../p2p.key",
|
||||
"/data/../p2p.key",
|
||||
] {
|
||||
let err = config_with_p2p_key(path)
|
||||
.validate()
|
||||
.expect_err(&format!("{path:?} names no directory and must be rejected"));
|
||||
assert!(
|
||||
err.contains("directory"),
|
||||
"{path:?} must be rejected for naming no directory, got: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The mirror of the above, and the case that stops the predicate widening
|
||||
/// into "reject every relative path". The shipped default is included on
|
||||
/// purpose: a predicate that rejects it is a boot failure for every node.
|
||||
#[test]
|
||||
fn p2p_key_path_naming_a_directory_is_accepted() {
|
||||
for path in [
|
||||
"keys/p2p.key",
|
||||
"./keys/p2p.key",
|
||||
"/data/keys/p2p.key",
|
||||
"/data/p2p.key",
|
||||
"~/.gitlawb/p2p.key",
|
||||
] {
|
||||
assert!(
|
||||
config_with_p2p_key(path).validate().is_ok(),
|
||||
"{path:?} names a directory and must be accepted"
|
||||
);
|
||||
}
|
||||
|
||||
Config::parse_from(["gitlawb-node"])
|
||||
.validate()
|
||||
.expect("the shipped default p2p key path must validate");
|
||||
}
|
||||
|
||||
/// The one input that separates validating the raw config string from
|
||||
/// validating `resolved_p2p_key_path()`. Raw, `~/` has an empty parent and
|
||||
/// would be rejected; resolved, it is the home directory, whose parent is a
|
||||
/// real directory, so it is accepted. Every other tilde path is accepted
|
||||
/// under both readings and therefore proves nothing.
|
||||
#[test]
|
||||
fn p2p_key_path_is_checked_after_tilde_expansion() {
|
||||
if dirs_next::home_dir().is_none() {
|
||||
panic!("this test needs a home directory to distinguish raw from resolved");
|
||||
}
|
||||
assert!(
|
||||
config_with_p2p_key("~/").validate().is_ok(),
|
||||
"`~/` resolves to the home directory, whose parent is a real directory"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -229,22 +229,35 @@ async fn main() -> Result<()> {
|
||||
.filter_map(|s| s.parse().ok())
|
||||
.collect();
|
||||
let shutdown_rx = shutdown_tx.subscribe();
|
||||
match p2p::start(
|
||||
&node_did.to_string(),
|
||||
config.p2p_port,
|
||||
bootstrap_addrs,
|
||||
Arc::clone(&db),
|
||||
config.auto_sync,
|
||||
shutdown_rx,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(handle) => {
|
||||
info!(port = config.p2p_port, peer_id = %handle.local_peer_id, "libp2p swarm started");
|
||||
Some(Arc::new(handle))
|
||||
match p2p::load_or_create_p2p_keypair(&config.resolved_p2p_key_path()) {
|
||||
Ok(local_key) => {
|
||||
match p2p::start(
|
||||
local_key,
|
||||
config.p2p_port,
|
||||
bootstrap_addrs,
|
||||
Arc::clone(&db),
|
||||
config.auto_sync,
|
||||
shutdown_rx,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(handle) => {
|
||||
info!(port = config.p2p_port, peer_id = %handle.local_peer_id, "libp2p swarm started");
|
||||
Some(Arc::new(handle))
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(err = %e, "failed to start libp2p swarm — continuing without p2p");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
// Deliberately non-fatal, and the cost is worth naming: an
|
||||
// unreadable key file takes the node off the p2p network for the
|
||||
// whole run while /health keeps reporting healthy, so the outage is
|
||||
// visible only to whoever reads the logs. Making it fatal, or
|
||||
// surfacing it in the health response, is its own change.
|
||||
Err(e) => {
|
||||
tracing::warn!(err = %e, "failed to start libp2p swarm — continuing without p2p");
|
||||
tracing::warn!(err = %e, "failed to load p2p identity key, continuing without p2p");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
+1194
-21
File diff suppressed because it is too large.
Load diff
@@ -12,6 +12,7 @@ primary_region = "iad"
|
||||
GITLAWB_P2P_PORT = "7546"
|
||||
GITLAWB_REPOS_DIR = "/data/repos"
|
||||
GITLAWB_KEY = "/data/keys/identity.pem"
|
||||
GITLAWB_P2P_KEY = "/data/keys/p2p.key"
|
||||
GITLAWB_PUBLIC_URL = "https://gitlawb-node-test.fly.dev"
|
||||
GITLAWB_BOOTSTRAP_PEERS = "https://node.gitlawb.com,https://node2.gitlawb.com,https://node3.gitlawb.com"
|
||||
GITLAWB_AUTO_SYNC = "true"
|
||||
|
||||
@@ -15,6 +15,7 @@ primary_region = 'sjc'
|
||||
GITLAWB_HOST = '0.0.0.0'
|
||||
GITLAWB_KEY = '/data/keys/identity.pem'
|
||||
GITLAWB_MAX_PACK_BYTES = '524288000'
|
||||
GITLAWB_P2P_KEY = '/data/keys/p2p.key'
|
||||
GITLAWB_P2P_PORT = '7546'
|
||||
GITLAWB_PORT = '7545'
|
||||
GITLAWB_PUBLIC_URL = 'https://node2.gitlawb.com'
|
||||
|
||||
@@ -15,6 +15,7 @@ primary_region = 'nrt'
|
||||
GITLAWB_HOST = '0.0.0.0'
|
||||
GITLAWB_KEY = '/data/keys/identity.pem'
|
||||
GITLAWB_MAX_PACK_BYTES = '524288000'
|
||||
GITLAWB_P2P_KEY = '/data/keys/p2p.key'
|
||||
GITLAWB_P2P_PORT = '7546'
|
||||
GITLAWB_PORT = '7545'
|
||||
GITLAWB_PUBLIC_URL = 'https://node3.gitlawb.com'
|
||||
|
||||
Reference in new issue
Block a user