Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/HBBS.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Create HBBS Windows

on:
workflow_dispatch:
pull_request:
branches:
- master
push:
branches:
- master

permissions:
contents: read

jobs:
build-windows:
name: Build hbbs.exe (Windows)
runs-on: windows-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc

- name: Build hbbs.exe
run: cargo build --release --target x86_64-pc-windows-msvc --bin hbbs

- name: Upload hbbs artifact
uses: actions/upload-artifact@v4
with:
name: hbbs-windows
path: target/x86_64-pc-windows-msvc/release/hbbs.exe
5 changes: 5 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ permissions:

on:
workflow_dispatch:
pull_request:
branches:
- master
push:
branches:
- master
Comment on lines +16 to +21
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Expect: pull_request is enabled, login steps are PR-gated, but publish steps still push.
echo "Trigger section:"
nl -ba .github/workflows/build.yaml | sed -n '14,25p'
echo
echo "Publish-related jobs/steps:"
rg -n -C2 "github\.event_name != 'pull_request'|push:\s*true|action-gh-release|docker-manifest-action" .github/workflows/build.yaml

Repository: rustdesk/rustdesk-server

Length of output: 8425


🏁 Script executed:

#!/bin/bash
# Get the exact trigger section
echo "=== Trigger section (lines 14-25) ==="
sed -n '14,25p' .github/workflows/build.yaml

echo -e "\n=== Job definitions (first job to understand structure) ==="
# Find job names and their triggers/gates
rg -n "^  [a-z-]+:" .github/workflows/build.yaml | head -15

Repository: rustdesk/rustdesk-server

Length of output: 589


🏁 Script executed:

#!/bin/bash
# Check job-level conditions for publish jobs
echo "=== Job-level conditions for docker/manifest/release/deb jobs ==="
for job in "docker:" "docker-manifest:" "docker-classic:" "docker-manifest-classic:" "release:" "deb-package:"; do
  echo "Job: $job"
  rg -A 5 "^  $job" .github/workflows/build.yaml | head -7
  echo
done

Repository: rustdesk/rustdesk-server

Length of output: 889


🏁 Script executed:

#!/bin/bash
# Check if docker build-push steps have conditions
echo "=== Docker job build step (around line 285-295) ==="
sed -n '280,300p' .github/workflows/build.yaml

echo -e "\n=== Docker-classic job build step (around line 450-465) ==="
sed -n '450,465p' .github/workflows/build.yaml

Repository: rustdesk/rustdesk-server

Length of output: 1687


🏁 Script executed:

#!/bin/bash
# Final verification: confirm there are no job-level "if" conditions on publish jobs
echo "=== Checking for job-level if conditions on publish jobs ==="
rg -B 2 "^  (docker|release|deb-package):" .github/workflows/build.yaml | grep -E "^\s*(if:|docker:|release:|deb-package:)" | head -20

Repository: rustdesk/rustdesk-server

Length of output: 164


Gate publish jobs on non-PR events.

Adding pull_request here enables the docker, docker-manifest, docker-classic, docker-manifest-classic, release, and deb-package jobs to run on PRs. The login steps correctly skip on PRs (gated with if: github.event_name != 'pull_request'), but the docker/build-push-action steps still execute with push: true without credentials, causing authentication failures. Either add if: github.event_name != 'pull_request' to these jobs or move them to a separate workflow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/build.yaml around lines 16 - 21, The workflow currently
triggers publish-related jobs on pull_request which causes
docker/build-push-action to run without credentials; for each publish job
(docker, docker-manifest, docker-classic, docker-manifest-classic, release,
deb-package) add an explicit job-level condition if: github.event_name !=
'pull_request' (or remove them from this workflow and put them into a separate
workflow that only triggers on push/release) so the docker/build-push-action
steps never execute during PR events.

tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
- '[0-9]+.[0-9]+.[0-9]+'
Expand Down
29 changes: 29 additions & 0 deletions .github/workflows/codex.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: codex

on:
workflow_dispatch:
inputs:
prompt:
description: "Tarea para Codex"
required: true
type: string
pull_request:
types: [opened, synchronize, reopened]

jobs:
codex:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
submodules: recursive

- name: Run Codex
uses: openai/codex-action@v1
with:
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
prompt: ${{ github.event.inputs.prompt || 'Review this PR and suggest fixes focused on rustdesk-server hbbs changes.' }}
Comment on lines +10 to +29
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

For GitHub Actions workflows triggered by pull_request, are repository secrets exposed to PRs from forks, and what minimum GITHUB_TOKENpermissions doesopenai/codex-action@v1 require?

💡 Result:

  • Are repository secrets exposed to pull_request runs from forks? No. For workflows triggered by pull_request from a fork, GitHub does not pass repository/org/environment secrets to the runner (the documented exception is the automatically-provided GITHUB_TOKEN). [1]

  • Minimum GITHUB_TOKEN permissions for openai/codex-action@v1: The action’s own README example runs openai/codex-action@v1 with only:

    permissions:
      contents: read

    i.e., contents: read is sufficient/minimal for the job running Codex (the example uses separate, more-privileged permissions only for a later step that posts PR feedback). [2]

Sources: [1] GitHub Docs – “Use secrets” (note about forks) (docs.github.com), [2] openai/codex-action README example (github.com)

Citations:


Guard fork PRs and trim the token scope.

This runs on every pull_request, but forked PRs will not receive OPENAI_API_KEY, causing the action to fail with a noisy red check. Gate the job on secret availability or limit to internal PRs. Additionally, openai/codex-action@v1 requires only contents: read and does not need write permissions; remove contents: write and pull-requests: write.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/codex.yml around lines 10 - 29, The workflow should skip
runs from forked PRs and reduce permissions: in the codex job (job id "codex")
add a guard such as if: ${{ github.event.pull_request.head.repo.fork == false }}
or if: ${{ secrets.OPENAI_API_KEY != '' }} to prevent runs when the secret is
unavailable, and change the job permissions to only contents: read (remove
contents: write) and drop pull-requests: write; keep the step using
openai/codex-action@v1 unchanged but rely on the guard to prevent failing on
forked PRs.

11 changes: 11 additions & 0 deletions src/peer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,15 @@ impl PeerMap {
pub(crate) async fn is_in_memory(&self, id: &str) -> bool {
self.map.read().await.contains_key(id)
}

#[inline]
pub(crate) async fn get_id_by_socket_addr(&self, addr: SocketAddr) -> Option<String> {
let map = self.map.read().await;
for (id, peer) in map.iter() {
if peer.read().await.socket_addr == addr {
return Some(id.clone());
}
}
None
}
}
55 changes: 52 additions & 3 deletions src/rendezvous_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ use hbb_common::{
use ipnetwork::Ipv4Network;
use sodiumoxide::crypto::sign;
use std::{
collections::HashMap,
collections::{HashMap, HashSet},
fs,
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
sync::atomic::{AtomicBool, AtomicUsize, Ordering},
sync::Arc,
Expand Down Expand Up @@ -88,6 +89,7 @@ pub struct RendezvousServer {
relay_servers0: Arc<RelayServers>,
rendezvous_servers: Arc<Vec<String>>,
inner: Arc<Inner>,
outbound_whitelist: HashSet<String>,
}

enum LoopFailure {
Expand Down Expand Up @@ -127,6 +129,19 @@ impl RendezvousServer {
.unwrap_or_default(),
)
};

let outbound_whitelist: HashSet<String> = fs::read_to_string("whitelist.txt")
.unwrap_or_default()
.lines()
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.collect();

log::info!(
"Loaded outbound whitelist entries: {}",
outbound_whitelist.len()
);
Comment on lines +133 to +143
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fail fast when whitelist.txt is missing.

read_to_string(...).unwrap_or_default() makes a missing or unreadable whitelist.txt look like an empty allowlist. Since the punch-hole path now rejects anything not in this set, a deployment without that file will break outbound connections with only a log entry. Please either fail fast on configuration errors or treat a missing file as “whitelist disabled”. The relative path makes this especially easy to hit under services and containers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/rendezvous_server.rs` around lines 133 - 143, The current use of
fs::read_to_string("whitelist.txt").unwrap_or_default() hides missing/unreadable
whitelist errors and creates an empty allowlist; change this to fail fast:
replace the unwrap_or_default usage around fs::read_to_string with explicit
error handling that logs a clear error including the filename and the io::Error
and then exits/returns an Err (e.g., log::error!("Failed to read whitelist.txt:
{}", err); std::process::exit(1)) so outbound_whitelist is only constructed when
the file is successfully read; reference the outbound_whitelist variable and the
read_to_string call when making this change.


let mut rs = Self {
tcp_punch: Arc::new(Mutex::new(HashMap::new())),
pm,
Expand All @@ -142,6 +157,7 @@ impl RendezvousServer {
mask,
local_ip,
}),
outbound_whitelist,
};
log::info!("mask: {:?}", rs.inner.mask);
log::info!("local-ip: {:?}", rs.inner.local_ip);
Expand Down Expand Up @@ -687,6 +703,7 @@ impl RendezvousServer {
ws: bool,
) -> ResultType<(RendezvousMessage, Option<SocketAddr>)> {
let mut ph = ph;

if !key.is_empty() && ph.licence_key != key {
log::warn!("Authentication failed from {} for peer {} - invalid key", addr, ph.id);
let mut msg_out = RendezvousMessage::new();
Expand All @@ -696,6 +713,38 @@ impl RendezvousServer {
});
return Ok((msg_out, None));
}

let source_id = self.pm.get_id_by_socket_addr(addr).await;
match source_id {
Some(src_id) => {
if !self.outbound_whitelist.contains(&src_id) {
log::warn!(
"Outbound connection rejected by whitelist: source_id={} remote_addr={}",
src_id,
addr
);
let mut msg_out = RendezvousMessage::new();
msg_out.set_punch_hole_response(PunchHoleResponse {
failure: punch_hole_response::Failure::ID_NOT_EXIST.into(),
..Default::default()
});
return Ok((msg_out, None));
}
}
None => {
log::warn!(
"Outbound connection rejected: unable to resolve source_id for remote_addr={}",
addr
);
let mut msg_out = RendezvousMessage::new();
msg_out.set_punch_hole_response(PunchHoleResponse {
failure: punch_hole_response::Failure::ID_NOT_EXIST.into(),
..Default::default()
});
return Ok((msg_out, None));
}
}
Comment on lines +717 to +746
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Socket-address auth will reject TCP/WebSocket callers.

addr here is the current transport endpoint, not a stable peer identity. TCP punch-hole requests arrive from a different socket than the UDP registration address, and in the websocket path Line 1219 and Line 1221 explicitly rewrite it to ip:0 / [ip]:0. That makes get_id_by_socket_addr return None for legitimate clients, so outbound punch-hole requests over TCP/WS get rejected. This check needs to use an authenticated peer/session ID instead of reverse-matching the socket address.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/rendezvous_server.rs` around lines 717 - 746, The current socket-address
based auth using get_id_by_socket_addr(addr) rejects legitimate TCP/WS callers
because addr is a transient transport endpoint; instead retrieve the
authenticated peer/session ID from the request context/session (the value used
for authentication earlier in your connection handling) and use that ID for
whitelist checks against outbound_whitelist and for constructing the
PunchHoleResponse; replace the get_id_by_socket_addr(addr) call and the
addr-based log messages with logic that reads the session/authenticated peer ID
(e.g. a variable like session_id or auth_peer_id produced during handshake) and
if missing return the same PunchHoleResponse but with logs and failure reason
keyed to missing/unauthenticated session rather than socket addr.


let id = ph.id;
// punch hole request from A, relay to B,
// check if in same intranet first,
Expand Down Expand Up @@ -723,7 +772,7 @@ impl RendezvousServer {
let to_id_clone = id.clone();
let mut lock = PUNCH_REQS.lock().await;
let mut dup = false;
for e in lock.iter().rev().take(30) { // only check recent tail subset for speed
for e in lock.iter().rev().take(30) {
if e.from_ip == from_ip && e.to_id == to_id_clone {
if e.tm.elapsed().as_secs() < PUNCH_REQ_DEDUPE_SEC { dup = true; }
break;
Expand Down Expand Up @@ -1053,7 +1102,7 @@ impl RendezvousServer {
let arg = fds.next();
if let Some("-") = arg { lock.clear(); }
else {
let mut start = arg.and_then(|x| x.parse::<usize>().ok()).unwrap_or(0);
let start = arg.and_then(|x| x.parse::<usize>().ok()).unwrap_or(0);
let mut page_size = fds.next().and_then(|x| x.parse::<usize>().ok()).unwrap_or(10);
if page_size == 0 { page_size = 10; }
for (_, e) in lock.iter().enumerate().skip(start).take(page_size) {
Expand Down