Skip to content

Commit a087651

Browse files
committed
use unsigned integers in database
1 parent a660d86 commit a087651

File tree

11 files changed

+167
-47
lines changed

11 files changed

+167
-47
lines changed

Cargo.toml

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,7 @@ parking_lot = "0.12.4"
3333
enum-utils = "0.1.2"
3434
flate2 = "1.1.2"
3535
tracing-appender = "0.2.3"
36-
sqlx = { version = "0.8.6", features = [
37-
"runtime-tokio",
38-
"postgres",
39-
"tls-rustls",
40-
"chrono",
41-
"uuid",
42-
] }
36+
sqlx = { version = "0.8.6", features = ["runtime-tokio", "postgres", "tls-rustls", "chrono", "uuid"] }
4337
rustc-hash = "2.1.1"
4438
mongodb = "3.3.0"
4539
eyre = "0.6.12"

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,20 @@ It is assumed that you know the basics of server scanning. Otherwise, I recommen
2929

3030
Rename `example-config.toml` to `config.toml` and fill in the fields.
3131

32-
Assuming you already have Postgres installed, you can make the database with the following queries:
32+
Installing Postgres with the [pg-uint128](https://github.com/pg-uint/pg-uint128) extension is required.
33+
34+
Then, can make the database with the following queries:
3335
```sql
3436
CREATE DATABASE matscan;
3537
CREATE USER matscan WITH PASSWORD 'replace me!!!';
3638
GRANT ALL PRIVILEGES ON DATABASE matscan TO matscan;
3739
GRANT CREATE ON SCHEMA public TO matscan;
3840
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO matscan;
41+
42+
-- the user will need superuser permissions on the first run to enable postgres extensions
43+
ALTER ROLE matscan superuser;
44+
-- after the first run, you should do ALTER ROLE matscan nosuperuser;
45+
3946
-- PostgreSQL URI is postgres://matscan:replace-me@localhost/matscan
4047
```
4148

migrations/20250913210747_uint.sql

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
-- migrate to unsigned integers to simplify some logic (especially when comparing ips/ports in the database)
2+
3+
-- requires pg-uint128 to be installed, see https://github.com/pg-uint/pg-uint128
4+
5+
create extension uint128;
6+
7+
alter table server_players drop constraint server_players_server_ip_server_port_fkey;
8+
alter table servers drop constraint servers_pkey;
9+
10+
alter table servers alter column ip set data type uint4 using ((ip::bigint + 4294967296::bigint) % 4294967296::bigint)::uint4;
11+
alter table servers alter column port set data type uint2 using ((port::bigint + 65536::bigint) % 65536::bigint)::uint2;
12+
alter table server_players alter column server_ip set data type uint4 using ((server_ip::bigint + 4294967296::bigint) % 4294967296::bigint)::uint4;
13+
alter table server_players alter column server_port set data type uint2 using ((server_port::bigint + 65536::bigint) % 65536::bigint)::uint2;
14+
alter table ips_with_aliased_servers alter column ip set data type uint4 using ((ip::bigint + 4294967296::bigint) % 4294967296::bigint)::uint4;
15+
16+
alter table servers add primary key (ip, port);
17+
alter table server_players
18+
add constraint server_players_server_ip_server_port_fkey
19+
foreign key (server_ip, server_port)
20+
references servers (ip, port)
21+
on delete cascade;

src/database/collect_servers.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use rustc_hash::FxHashMap;
99
use sqlx::Row;
1010
use tracing::info;
1111

12-
use crate::database::Database;
12+
use crate::database::{Database, PgU16, PgU32};
1313

1414
#[derive(Default)]
1515
pub struct CollectServersCache {
@@ -118,8 +118,8 @@ impl Database {
118118

119119
let mut servers = Vec::new();
120120
while let Some(row) = rows.try_next().await? {
121-
let ip = Ipv4Addr::from_bits(row.get::<i32, _>(0) as u32);
122-
let port = row.get::<i16, _>(1) as u16;
121+
let ip = Ipv4Addr::from_bits(row.get::<PgU32, _>(0).0);
122+
let port = row.get::<PgU16, _>(1).0;
123123

124124
servers.push(SocketAddrV4::new(ip, port));
125125

src/database/migrate_mongo_to_postgres.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ use futures_util::TryStreamExt;
88
use sqlx::QueryBuilder;
99
use uuid::Uuid;
1010

11+
use crate::database::{PgU16, PgU32};
12+
1113
pub async fn do_migration(mongodb_uri: &str, postgres_uri: &str) {
1214
let client_options = mongodb::options::ClientOptions::parse(mongodb_uri)
1315
.await
@@ -294,8 +296,8 @@ impl Database {
294296
last_time_no_players_online = $27
295297
"#,
296298
)
297-
.bind(addr.ip().to_bits() as i32)
298-
.bind(addr.port() as i16)
299+
.bind(PgU32(addr.ip().to_bits()) )
300+
.bind(PgU16(addr.port()))
299301
.bind(s.last_pinged)
300302
.bind(s.is_online_mode)
301303
.bind(s.favicon_hash)
@@ -330,8 +332,8 @@ impl Database {
330332
"INSERT INTO server_players (server_ip, server_port, uuid, username, online_mode, last_seen, first_seen) ",
331333
);
332334
query_builder.push_values(s.player_sample, |mut b, player| {
333-
b.push_bind(addr.ip().to_bits() as i32)
334-
.push_bind(addr.port() as i16)
335+
b.push_bind(PgU32(addr.ip().to_bits()))
336+
.push_bind(PgU16(addr.port()))
335337
.push_bind(player.uuid)
336338
.push_bind(player.name.map(|n| n.replace('\0', "")))
337339
.push_bind(player.uuid.map(|u| match u.get_version_num() {

src/database/mod.rs

Lines changed: 99 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,26 @@
11
pub mod collect_servers;
22
pub mod migrate_mongo_to_postgres;
33

4-
use std::{collections::HashSet, net::Ipv4Addr, sync::Arc, time::Duration};
4+
use std::{
5+
collections::HashSet,
6+
fmt::{self, Display},
7+
net::Ipv4Addr,
8+
ops::Deref,
9+
str::FromStr,
10+
sync::Arc,
11+
time::Duration,
12+
};
513

614
use futures_util::stream::StreamExt;
715
use lru_cache::LruCache;
816
use parking_lot::Mutex;
917
use rustc_hash::FxHashMap;
10-
use sqlx::{PgPool, Row};
18+
use sqlx::{
19+
PgPool, Postgres, Row,
20+
encode::IsNull,
21+
error::BoxDynError,
22+
postgres::{PgArgumentBuffer, PgTypeInfo, PgValueFormat, PgValueRef},
23+
};
1124
use tracing::{error, info};
1225

1326
use crate::database::collect_servers::CollectServersCache;
@@ -80,8 +93,8 @@ impl Database {
8093
.fetch_all(&self.pool)
8194
.await?;
8295
for row in rows {
83-
let ip = Ipv4Addr::from_bits(row.get::<i32, _>(0) as u32);
84-
let allowed_port = row.get::<i16, _>(1) as u16;
96+
let ip = Ipv4Addr::from_bits(row.get::<PgU32, _>(0).0);
97+
let allowed_port = row.get::<PgU16, _>(1).0;
8598
aliased_ips_to_allowed_port.insert(ip, allowed_port);
8699
}
87100
self.shared.lock().aliased_ips_to_allowed_port = aliased_ips_to_allowed_port;
@@ -102,14 +115,14 @@ impl Database {
102115
let mut txn = self.pool.begin().await?;
103116

104117
sqlx::query("INSERT INTO ips_with_aliased_servers (ip, allowed_port) VALUES ($1, $2)")
105-
.bind(ip.to_bits() as i32)
106-
.bind(allowed_port as i16)
118+
.bind(PgU32(ip.to_bits()))
119+
.bind(PgU16(allowed_port))
107120
.execute(&mut *txn)
108121
.await?;
109122
// delete all servers with this ip that aren't on the allowed port
110123
let delete_res = sqlx::query("DELETE FROM servers WHERE ip = $1 AND port != $2")
111-
.bind(ip.to_bits() as i32)
112-
.bind(allowed_port as i16)
124+
.bind(PgU32(ip.to_bits()))
125+
.bind(PgU16(allowed_port))
113126
.execute(&mut *txn)
114127
.await?;
115128
let deleted_count = delete_res.rows_affected();
@@ -146,7 +159,7 @@ impl Database {
146159
.fetch(&self.pool);
147160

148161
while let Some(Ok(row)) = rows.next().await {
149-
let ip = row.get::<i32, _>(0);
162+
let ip = row.get::<PgU32, _>(0).0;
150163
let player_count = row.get::<i64, _>(1);
151164

152165
let delete_count = player_count - KEEP_PLAYER_COUNT;
@@ -160,7 +173,7 @@ impl Database {
160173
)
161174
",
162175
)
163-
.bind(ip)
176+
.bind(PgU32(ip))
164177
.bind(delete_count)
165178
.execute(&self.pool)
166179
.await?;
@@ -174,3 +187,79 @@ impl Database {
174187
pub fn sanitize_text_for_postgres(s: &str) -> String {
175188
s.replace('\0', "")
176189
}
190+
191+
pub struct PgU32(pub u32);
192+
impl Deref for PgU32 {
193+
type Target = u32;
194+
fn deref(&self) -> &Self::Target {
195+
&self.0
196+
}
197+
}
198+
impl Display for PgU32 {
199+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200+
write!(f, "{}", self.0)
201+
}
202+
}
203+
impl FromStr for PgU32 {
204+
type Err = std::num::ParseIntError;
205+
fn from_str(s: &str) -> Result<Self, Self::Err> {
206+
Ok(Self(s.parse()?))
207+
}
208+
}
209+
impl sqlx::Type<Postgres> for PgU32 {
210+
fn type_info() -> PgTypeInfo {
211+
PgTypeInfo::with_name("uint4")
212+
}
213+
}
214+
impl sqlx::Decode<'_, Postgres> for PgU32 {
215+
fn decode(value: PgValueRef<'_>) -> Result<Self, BoxDynError> {
216+
Ok(match value.format() {
217+
PgValueFormat::Binary => Self(u32::from_be_bytes(value.as_bytes()?.try_into()?)),
218+
PgValueFormat::Text => value.as_str()?.parse()?,
219+
})
220+
}
221+
}
222+
impl sqlx::Encode<'_, Postgres> for PgU32 {
223+
fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
224+
buf.extend(&self.to_be_bytes());
225+
Ok(IsNull::No)
226+
}
227+
}
228+
229+
pub struct PgU16(pub u16);
230+
impl Deref for PgU16 {
231+
type Target = u16;
232+
fn deref(&self) -> &Self::Target {
233+
&self.0
234+
}
235+
}
236+
impl Display for PgU16 {
237+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
238+
write!(f, "{}", self.0)
239+
}
240+
}
241+
impl FromStr for PgU16 {
242+
type Err = std::num::ParseIntError;
243+
fn from_str(s: &str) -> Result<Self, Self::Err> {
244+
Ok(Self(s.parse()?))
245+
}
246+
}
247+
impl sqlx::Type<Postgres> for PgU16 {
248+
fn type_info() -> PgTypeInfo {
249+
PgTypeInfo::with_name("uint2")
250+
}
251+
}
252+
impl sqlx::Decode<'_, Postgres> for PgU16 {
253+
fn decode(value: PgValueRef<'_>) -> Result<Self, BoxDynError> {
254+
Ok(match value.format() {
255+
PgValueFormat::Binary => Self(u16::from_be_bytes(value.as_bytes()?.try_into()?)),
256+
PgValueFormat::Text => value.as_str()?.parse()?,
257+
})
258+
}
259+
}
260+
impl sqlx::Encode<'_, Postgres> for PgU16 {
261+
fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
262+
buf.extend(&self.to_be_bytes());
263+
Ok(IsNull::No)
264+
}
265+
}

src/net/raw_sockets.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ use std::{
77

88
#[repr(C)]
99
#[derive(Debug, Clone)]
10-
struct ifreq {
10+
struct Ifreq {
1111
ifr_name: [libc::c_char; libc::IF_NAMESIZE],
1212
ifr_data: libc::c_int, /* ifr_ifindex or ifr_mtu */
1313
}
1414

15-
fn ifreq_for(name: &str) -> ifreq {
16-
let mut ifreq = ifreq {
15+
fn ifreq_for(name: &str) -> Ifreq {
16+
let mut ifreq = Ifreq {
1717
ifr_name: [0; libc::IF_NAMESIZE],
1818
ifr_data: 0,
1919
};
@@ -25,11 +25,11 @@ fn ifreq_for(name: &str) -> ifreq {
2525

2626
fn ifreq_ioctl(
2727
lower: libc::c_int,
28-
ifreq: &mut ifreq,
28+
ifreq: &mut Ifreq,
2929
cmd: libc::c_ulong,
3030
) -> io::Result<libc::c_int> {
3131
unsafe {
32-
let res = libc::ioctl(lower, cmd as _, ifreq as *mut ifreq);
32+
let res = libc::ioctl(lower, cmd as _, ifreq as *mut Ifreq);
3333
if res == -1 {
3434
return Err(io::Error::last_os_error());
3535
}
@@ -47,7 +47,7 @@ pub const ETH_P_IEEE802154: libc::c_short = 0x00F6;
4747
pub struct RawSocket {
4848
protocol: libc::c_short,
4949
lower: libc::c_int,
50-
ifreq: ifreq,
50+
ifreq: Ifreq,
5151
}
5252

5353
impl AsRawFd for RawSocket {

src/processing.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ use sqlx::Row;
1616
use tokio::time::sleep;
1717

1818
use crate::{
19-
config::Config, database::Database, processing::minecraft::SamplePlayer, terminal_colors::*,
19+
config::Config,
20+
database::{Database, PgU16, PgU32},
21+
processing::minecraft::SamplePlayer,
22+
terminal_colors::*,
2023
};
2124

2225
pub struct SharedData {
@@ -132,8 +135,8 @@ async fn handle_response_futures(
132135
tasks.push(async move {
133136
let mut processed_server_status = if let Ok(row) =
134137
sqlx::query("SELECT last_pinged FROM servers WHERE ip = $1 AND port = $2")
135-
.bind(addr.ip().to_bits() as i32)
136-
.bind(addr.port() as i16)
138+
.bind(PgU32(addr.ip().to_bits()))
139+
.bind(PgU16(addr.port()))
137140
.fetch_one(&db.pool)
138141
.await
139142
{

src/processing/minecraft/mod.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use uuid::Uuid;
2121
use super::{ProcessableProtocol, SharedData};
2222
use crate::{
2323
config::Config,
24-
database::{CachedIpHash, Database, sanitize_text_for_postgres},
24+
database::{CachedIpHash, Database, PgU16, PgU32, sanitize_text_for_postgres},
2525
processing::minecraft::{
2626
passive_fingerprint::{PassiveMinecraftFingerprint, generate_passive_fingerprint},
2727
snipe::maybe_log_sniped,
@@ -335,8 +335,8 @@ pub async fn insert_server_to_db(
335335

336336
let mut qb = InsertServerQueryBuilder::new();
337337
let now = chrono::Utc::now();
338-
qb.field("ip", target.ip().to_bits() as i32);
339-
qb.field("port", target.port() as i16);
338+
qb.field("ip", PgU32(target.ip().to_bits()));
339+
qb.field("port", PgU16(target.port()));
340340
qb.field("last_pinged", now);
341341
qb.field("is_online_mode", r.is_online_mode);
342342
qb.field("favicon_hash", r.favicon_hash);
@@ -383,8 +383,8 @@ pub async fn insert_server_to_db(
383383
"INSERT INTO server_players (server_ip, server_port, uuid, username, online_mode, last_seen) ",
384384
);
385385
query_builder.push_values(&r.player_sample, |mut b, player| {
386-
b.push_bind(target.ip().to_bits() as i32)
387-
.push_bind(target.port() as i16)
386+
b.push_bind(PgU32(target.ip().to_bits()))
387+
.push_bind(PgU16(target.port()))
388388
.push_bind(player.uuid)
389389
.push_bind(player.name.clone())
390390
.push_bind(match player.uuid.get_version_num() {

src/processing/minecraft/snipe.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::{collections::HashMap, net::SocketAddrV4, sync::Arc};
22

33
use parking_lot::Mutex;
44

5-
use crate::{config::Config, database::Database, processing::{SharedData, minecraft::{ANONYMOUS_PLAYER_NAME, PingResponse, }}};
5+
use crate::{config::Config, database::{Database, PgU16, PgU32}, processing::{SharedData, minecraft::{ANONYMOUS_PLAYER_NAME, PingResponse, }}};
66

77
pub fn maybe_log_sniped(
88
shared: &Arc<Mutex<SharedData>>,
@@ -98,8 +98,8 @@ pub fn maybe_log_sniped(
9898
"SELECT FROM server_players WHERE username = '$1' AND server_ip = $2 AND server_port = $3 LIMIT 1",
9999
)
100100
.bind(ANONYMOUS_PLAYER_NAME)
101-
.bind(target.ip().to_bits() as i32)
102-
.bind(target.port() as i16).fetch_optional(&database.pool).await {
101+
.bind(PgU32(target.ip().to_bits()))
102+
.bind(PgU16(target.port())).fetch_optional(&database.pool).await {
103103
let has_historical_anon = has_historical_anon_res.is_some();
104104
if !has_historical_anon {
105105
send_to_webhook(

0 commit comments

Comments
 (0)