initial commit

This commit is contained in:
yggverse 2025-06-30 13:46:39 +03:00
parent 36556f4f3c
commit 1392fee475
10 changed files with 348 additions and 0 deletions

1
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1 @@
custom: https://yggverse.github.io/#donate

27
.github/workflows/linux.yml vendored Normal file
View file

@ -0,0 +1,27 @@
name: Linux
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -Dwarnings
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run rustfmt
run: cargo fmt --all -- --check
- name: Run clippy
run: cargo clippy --all-targets
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
Cargo.lock

16
Cargo.toml Normal file
View file

@ -0,0 +1,16 @@
[package]
name = "yps"
version = "0.1.0"
edition = "2024"
license = "MIT"
readme = "README.md"
description = "YPS - Yggdrasil Port Scanner"
keywords = ["yggdrasil", "tcp", "udp", "search", "crawler"]
categories = ["network-programming"]
repository = "https://github.com/YGGverse/yps"
[dependencies]
anyhow = "1.0"
clap = { version = "4.5", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

43
README.md Normal file
View file

@ -0,0 +1,43 @@
# YPS - Yggdrasil Port Scanner
![Linux](https://github.com/yggverse/yps/actions/workflows/linux.yml/badge.svg)
[![Dependencies](https://deps.rs/repo/github/yggverse/yps/status.svg)](https://deps.rs/repo/github/yggverse/yps)
[![crates.io](https://img.shields.io/crates/v/yps)](https://crates.io/crates/yps)
A simple crawler tool to find open ports for specific services,
such as web servers or Bitcoin nodes, to connect with [Yggdrasil](https://yggdrasil-network.github.io/).
## Install
* `git clone https://github.com/yggverse/yps.git && cd yps`
* `cargo build --release`
* `sudo install target/release/yps /usr/local/bin/yps`
## Usage
``` bash
sudo yps --tcp -p 80 -p 443
```
### Options
``` bash
-d, --debug
Print debug information
-s, --socket <SOCKET>
Yggdrasil socket path (unix only) [default: /var/run/yggdrasil.sock]
-p, --port <PORT>
Port(s) to scan
-u, --udp <UDP>
Crawl UDP ports (requires `[host]:port` binding as the value)
-t, --tcp
Crawl TCP ports
-l, --latency <LATENCY>
Pause in seconds before make new request
--index-capacity <INDEX_CAPACITY>
Optimize memory usage by providing expecting index capacity, according to the current network size [default: 1000]
-h, --help
Print help
-V, --version
Print version
```

34
src/config.rs Normal file
View file

@ -0,0 +1,34 @@
use clap::Parser;
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
pub struct Config {
/// Print debug information
#[arg(short, long, default_value_t = false)]
pub debug: bool,
/// Yggdrasil socket path (unix only)
#[arg(short, long, default_value_t = String::from("/var/run/yggdrasil.sock"))]
pub socket: String,
/// Port(s) to scan
#[arg(short, long)]
pub port: Vec<u16>,
/// Crawl UDP ports (requires `[host]:port` binding as the value)
#[arg(short, long)]
pub udp: Option<String>,
/// Crawl TCP ports
#[arg(short, long, default_value_t = false)]
pub tcp: bool,
/// Pause in seconds before make new request
#[arg(short, long)]
pub latency: Option<u64>,
/// Optimize memory usage by providing expecting index capacity,
/// according to the current network size
#[arg(long, default_value_t = 1000)]
pub index_capacity: usize,
}

101
src/main.rs Normal file
View file

@ -0,0 +1,101 @@
mod config;
mod udp;
mod yggdrasil;
use anyhow::Result;
use config::Config;
use std::{
net::{IpAddr, SocketAddr, TcpStream},
time::Duration,
};
use udp::Udp;
use yggdrasil::Yggdrasil;
fn main() -> Result<()> {
use clap::Parser;
let config = Config::parse();
if config.port.is_empty() {
panic!("at least one port must be specified for scan!")
}
if !config.tcp && config.udp.is_none() {
panic!("at least one TCP or UDP protocol is required for scan!")
}
let latency = config.latency.map(Duration::from_secs); // parse once
let mut ygg = Yggdrasil::init(&config.socket)?;
let mut tcp: Option<Vec<SocketAddr>> = if config.tcp {
Some(Vec::with_capacity(config.index_capacity))
} else {
None
};
let mut udp = config.udp.as_ref().map(|bind| {
let server = Udp::init(bind).unwrap();
let index: Vec<SocketAddr> = Vec::with_capacity(config.index_capacity);
(index, server)
});
// get initial peers to crawl
if config.debug {
println!("get initial peers to crawl...");
}
let p = ygg.peers()?;
if p.status != "success" {
todo!()
}
// start crawler
for peer in p.response.unwrap().peers {
crawl(peer.key, latency, &config, &mut ygg, &mut tcp, &mut udp)?;
}
Ok(())
}
fn crawl(
key: String,
latency: Option<Duration>,
config: &Config,
ygg: &mut Yggdrasil,
tcp: &mut Option<Vec<SocketAddr>>,
udp: &mut Option<(Vec<SocketAddr>, Udp)>,
) -> Result<()> {
if config.debug {
println!("get peers for `{}`...", &key);
}
let p = ygg.remote_peers(key)?;
if p.status != "success" {
todo!()
}
for (host, peers) in p.response.unwrap() {
for port in &config.port {
let address = SocketAddr::new(IpAddr::V6(host), *port);
if let Some(index) = tcp {
if !index.contains(&address) {
let url = format!("tcp://{address}");
if config.debug {
println!("try `{url}`...");
}
if TcpStream::connect_timeout(&address, Duration::from_secs(1)).is_ok() {
println!("{url}");
index.push(address)
}
}
}
if let Some((index, server)) = udp {
let url = format!("udp://{address}");
if !index.contains(&address) {
if config.debug {
println!("try `{url}`...");
}
if server.check(address) {
println!("{url}");
index.push(address)
}
}
}
}
for k in peers.keys {
if let Some(l) = latency {
std::thread::sleep(l);
}
crawl(k, latency, config, ygg, tcp, udp)?;
}
}
Ok(())
}

33
src/udp.rs Normal file
View file

@ -0,0 +1,33 @@
use anyhow::Result;
use std::{
net::{SocketAddr, SocketAddrV6, UdpSocket},
str::FromStr,
time::Duration,
};
/// Simple UDP server to test peers connection
pub struct Udp(UdpSocket);
impl Udp {
pub fn init(bind: &str) -> Result<Self> {
const T: u64 = 1; // @TODO optional
let this = UdpSocket::bind(SocketAddrV6::from_str(bind)?)?;
this.set_write_timeout(Some(Duration::from_secs(T)))?;
this.set_read_timeout(Some(Duration::from_secs(T)))?;
Ok(Self(this))
}
pub fn check(&self, remote: SocketAddr) -> bool {
if self.0.connect(remote).is_err() {
return false;
}
match self.0.send_to(b"test", remote) {
Ok(_) => {
let mut b = [0; 1];
self.0.recv_from(&mut b).is_ok() // @TODO
}
Err(_) => false,
}
}
}

50
src/yggdrasil.rs Normal file
View file

@ -0,0 +1,50 @@
mod api;
use api::*;
use anyhow::Result;
use std::{
collections::HashMap,
io::{Read, Write},
net::Ipv6Addr,
os::unix::net::UnixStream,
};
pub struct Yggdrasil(UnixStream);
impl Yggdrasil {
pub fn init(path: &str) -> Result<Self> {
Ok(Self(UnixStream::connect(path)?))
}
pub fn peers(&mut self) -> Result<Response<LocalPeers>> {
let r: Response<LocalPeers> = serde_json::from_slice(
&self.request(b"{\"keepalive\":true,\"request\":\"getpeers\"}")?,
)?;
Ok(r)
}
pub fn remote_peers(
&mut self,
public_key: String,
) -> Result<Response<HashMap<Ipv6Addr, RemotePeers>>> {
let r: Response<HashMap<Ipv6Addr, RemotePeers>> = serde_json::from_slice(&self.request(
format!("{{\"keepalive\":true,\"request\":\"debug_remotegetpeers\",\"arguments\":{{\"key\":\"{public_key}\"}}}}")
.as_bytes(),
)?)?;
Ok(r)
}
fn request(&mut self, bytes: &[u8]) -> Result<Vec<u8>> {
const L: usize = 1024; // chunk len
self.0.write_all(bytes)?;
let mut r = Vec::with_capacity(L);
loop {
let mut b = vec![0; L];
let l = self.0.read(&mut b)?; // can't `read_to_end` as no EOF
r.extend(&b[..l]);
if l < L {
return Ok(r);
}
}
}
}

41
src/yggdrasil/api.rs Normal file
View file

@ -0,0 +1,41 @@
use serde::Deserialize;
#[derive(Deserialize, Debug)]
pub struct Response<T> {
pub response: Option<T>,
pub status: String,
}
#[derive(Deserialize, Debug)]
pub struct LocalPeers {
pub peers: Vec<LocalPeer>,
}
#[derive(Deserialize, Debug)]
pub struct LocalPeer {
pub key: String,
// pub address: std::net::Ipv6Addr,
// pub bytes_recvd: Option<u64>,
// pub bytes_sent: Option<u64>,
// pub inbound: bool,
// #[serde(default, deserialize_with = "time")]
// pub last_error_time: Option<Duration>,
// pub last_error: Option<String>,
// #[serde(default, deserialize_with = "time")]
// pub latency: Option<Duration>,
// pub port: u64,
// pub priority: Option<u64>,
// pub remote: Option<String>,
// pub up: bool,
// pub uptime: Option<f64>,
}
#[derive(Deserialize, Debug)]
pub struct RemotePeers {
pub keys: Vec<String>,
}
// use std::time::Duration;
// fn time<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Option<Duration>, D::Error> {
// u64::deserialize(d).map(|t| Some(Duration::from_nanos(t)))
// }