Avaldatud allikas
Laadi alla allikas ZIP
CH-J Tools
Vaata katalooge ja faile konkreetse rakenduse vabastamiseks.
CH-J / Server Manager
1704 failid
0.1.2-beta
Lisatud: · Ehitatud:
1
use serde_json::{json, Value};2
use std::collections::{BTreeMap, BTreeSet};3
use std::net::IpAddr;5
pub fn sections(stdout: &str) -> Vec<(String, String)> {6
let mut result: Vec<(String, String)> = Vec::new();7
for line in stdout.split_inclusive('\n') {8
if let Some(name) = line9
.trim_end_matches(['\r', '\n'])10
.strip_prefix("\x1eCHJ_SECTION=")11
{12
result.push((name.to_owned(), String::new()));13
} else if let Some((_, body)) = result.last_mut() {14
body.push_str(line);15
}16
}17
result18
}20
pub(crate) fn body<'a>(parts: &'a [(String, String)], name: &str) -> &'a str {21
parts22
.iter()23
.find(|(n, _)| n == name)24
.map(|(_, b)| b.as_str())25
.unwrap_or("")26
}28
pub(crate) fn clean(s: &str) -> &str {29
s.trim().trim_matches(['\'', '"'])30
}32
fn scalar(s: &str) -> Value {33
if s.is_empty() {34
Value::Null35
} else {36
json!(s)37
}38
}40
pub(crate) fn pairs(text: &str) -> BTreeMap<String, String> {41
text.lines()42
.filter(|l| !l.trim_start().starts_with('#'))43
.filter_map(|l| {44
let (k, v) = l.split_once('=')?;45
Some((k.trim().to_owned(), clean(v).to_owned()))46
})47
.collect()48
}50
pub(crate) fn ini(text: &str) -> BTreeMap<String, String> {51
let mut result = BTreeMap::new();52
let mut group = "";53
for l in text54
.lines()55
.map(str::trim)56
.filter(|l| !l.starts_with(['#', ';']))57
{58
if l.starts_with('[') && l.ends_with(']') {59
group = &l[1..l.len() - 1];60
} else if let Some((k, v)) = l.split_once('=') {61
let key = format!("{}.{}", group, k.trim());62
result63
.entry(key)64
.and_modify(|old: &mut String| {65
old.push(' ');66
old.push_str(clean(v));67
})68
.or_insert_with(|| clean(v).to_owned());69
}70
}71
result72
}74
pub(crate) fn words(s: &str) -> Vec<String> {75
s.split([' ', '\t', ';', ','])76
.map(clean)77
.filter(|s| !s.is_empty())78
.map(str::to_owned)79
.collect()80
}82
pub(crate) fn finding(level: &str, code: &str, params: Value) -> Value {83
json!({"level": level, "code": code, "params": params})84
}86
#[derive(Default, Debug)]87
pub(crate) struct Uci {88
pub kind: String,89
pub name: String,90
pub fields: BTreeMap<String, String>,91
pub raw: String,92
}94
pub(crate) fn uci(text: &str) -> Vec<Uci> {95
let mut blocks: Vec<Uci> = Vec::new();96
for line in text.split_inclusive('\n') {97
let t = line.trim();98
if t.is_empty() || t.starts_with('#') {99
if let Some(b) = blocks.last_mut() {100
b.raw.push_str(line);101
}102
continue;103
}104
if let Some(rest) = t.strip_prefix("config ") {105
let mut w = rest.split_whitespace();106
blocks.push(Uci {107
kind: w.next().unwrap_or("").to_owned(),108
name: clean(w.next().unwrap_or("")).to_owned(),109
raw: line.to_owned(),110
..Uci::default()111
});112
} else if t.starts_with("option ")113
|| t.starts_with("list ")114
|| t.starts_with("option\t")115
|| t.starts_with("list\t")116
{117
if let Some(b) = blocks.last_mut() {118
b.raw.push_str(line);119
let rest = t120
.split_once(char::is_whitespace)121
.map(|(_, v)| v.trim())122
.unwrap_or("");123
if let Some((k, v)) = rest.split_once(char::is_whitespace) {124
let v = clean(v).to_owned();125
if t.starts_with("list") {126
b.fields127
.entry(k.to_owned())128
.and_modify(|old| {129
old.push(' ');130
old.push_str(&v);131
})132
.or_insert(v);133
} else {134
b.fields.insert(k.to_owned(), v);135
}136
}137
}138
} else if let Some((left, right)) = t.split_once('=') {139
let mut p = left.splitn(3, '.');140
let _package = p.next();141
let name = p.next().unwrap_or("");142
let key = p.next();143
let idx = blocks144
.iter()145
.position(|b| b.name == name)146
.unwrap_or_else(|| {147
blocks.push(Uci {148
name: name.to_owned(),149
..Uci::default()150
});151
blocks.len() - 1152
});153
let b = &mut blocks[idx];154
b.raw.push_str(line);155
if let Some(k) = key {156
b.fields.insert(157
k.to_owned(),158
right159
.split_whitespace()160
.map(clean)161
.collect::<Vec<_>>()162
.join(" "),163
);164
} else {165
b.kind = clean(right).to_owned();166
}167
}168
}169
blocks170
}172
fn item<'a>(m: &'a BTreeMap<String, String>, k: &str) -> &'a str {173
m.get(k).map(String::as_str).unwrap_or("")174
}175
fn after<'a>(w: &[&'a str], k: &str) -> Option<&'a str> {176
w.windows(2).find(|p| p[0] == k).map(|p| p[1])177
}178
fn number(s: &str) -> Value {179
s.parse::<u64>().ok().map_or(Value::Null, |n| json!(n))180
}181
fn flag(v: &Value, name: &str) -> bool {182
v["flags"]183
.as_array()184
.is_some_and(|a| a.iter().any(|x| x == name))185
}187
fn interface(name: &str) -> Value {188
json!({"name": name, "kind": "other", "physical": null, "state": "unknown", "carrier": null,189
"speedMbps": null, "duplex": null, "mtu": null, "mac": null, "master": null, "vlanId": null,190
"parent": null, "addresses": [], "ipv4Method": "unknown", "ipv6Method": "unknown",191
"methodSource": null, "configuredIn": null, "managedBy": null, "gateway4": null,192
"gateway6": null, "metric": null, "dns": [], "ssh": false})193
}195
fn address(v: &Value) -> Option<Value> {196
let family = match v["family"].as_str()? {197
"inet" => 4,198
"inet6" => 6,199
_ => return None,200
};201
let addr = v["local"].as_str()?;202
Some(203
json!({"family": family, "address": addr, "prefix": v["prefixlen"], "scope": v["scope"],204
"dynamic": v["dynamic"].as_bool().unwrap_or(false), "secondary": v["secondary"].as_bool().unwrap_or(false),205
"temporary": v["temporary"].as_bool().unwrap_or(false), "lifetimeValid": v["valid_life_time"]}),206
)207
}209
fn link(v: &Value, iface: &mut Value) {210
for (src, dst) in [("mtu", "mtu"), ("address", "mac"), ("master", "master")] {211
if !v[src].is_null() {212
iface[dst] = v[src].clone();213
}214
}215
iface["state"] = json!(match v["operstate"]216
.as_str()217
.unwrap_or("")218
.to_ascii_lowercase()219
.as_str()220
{221
"up" => "up",222
"down" => "down",223
_ => "unknown",224
});225
let kind = v["linkinfo"]["info_kind"].as_str().unwrap_or("");226
iface["kind"] = json!(match kind {227
"bridge" | "bond" | "vlan" | "wireguard" | "tun" | "veth" | "dummy" => kind,228
_ if v["link_type"] == "loopback" || flag(v, "LOOPBACK") => "loopback",229
_ if v["link_type"] == "ether" => "ethernet",230
_ => "other",231
});232
if !v["linkinfo"]["info_data"]["id"].is_null() {233
iface["vlanId"] = v["linkinfo"]["info_data"]["id"].clone();234
}235
if !v["link"].is_null() {236
iface["parent"] = v["link"].clone();237
}238
if !v["link_index"].is_null() {239
iface["parent"] = v["link_index"].clone();240
}241
}243
fn interfaces(parts: &[(String, String)]) -> Vec<Value> {244
let mut result: Vec<Value> = Vec::new();245
let mut indexes = BTreeMap::new();246
for section in ["addr-json", "link-json"] {247
if let Ok(Value::Array(list)) = serde_json::from_str::<Value>(body(parts, section)) {248
for v in list {249
let Some(name) = v["ifname"].as_str() else {250
continue;251
};252
if let Some(idx) = v["ifindex"].as_u64() {253
indexes.insert(idx, name.to_owned());254
}255
let idx = ensure(&mut result, name);256
link(&v, &mut result[idx]);257
if let Some(addrs) = v["addr_info"].as_array() {258
result[idx]["addresses"] =259
json!(addrs.iter().filter_map(address).collect::<Vec<_>>());260
}261
}262
}263
}264
for section in ["addr-text", "link-text"] {265
text_interfaces(body(parts, section), &mut result);266
}267
for (name, text) in parts {268
let Some(name) = name.strip_prefix("sys:") else {269
continue;270
};271
let m = pairs(text);272
// bonding_masters is a sysfs control file, not a network device.273
if item(&m, "mtu").is_empty() && item(&m, "address").is_empty() {274
continue;275
}276
let idx = ensure(&mut result, name);277
let iface = &mut result[idx];278
iface["physical"] = match item(&m, "physical") {279
"1" => json!(true),280
"0" => json!(false),281
_ => Value::Null,282
};283
iface["carrier"] = match item(&m, "carrier") {284
"1" => json!(true),285
"0" => json!(false),286
_ => Value::Null,287
};288
iface["speedMbps"] = item(&m, "speed")289
.parse::<u64>()290
.ok()291
.filter(|n| *n > 0)292
.map_or(Value::Null, |n| json!(n));293
iface["duplex"] = match item(&m, "duplex") {294
"full" => json!("full"),295
"half" => json!("half"),296
_ => Value::Null,297
};298
if !item(&m, "mtu").is_empty() {299
iface["mtu"] = number(item(&m, "mtu"));300
}301
if !item(&m, "address").is_empty() {302
iface["mac"] = json!(item(&m, "address"));303
}304
if !item(&m, "operstate").is_empty() {305
iface["state"] = json!(match item(&m, "operstate") {306
"up" => "up",307
"down" => "down",308
_ => "unknown",309
});310
}311
}312
for iface in &mut result {313
for field in ["master", "parent"] {314
if let Some(idx) = iface[field].as_u64() {315
iface[field] = indexes.get(&idx).map_or(Value::Null, |s| json!(s));316
}317
}318
}319
result320
}322
fn ensure(list: &mut Vec<Value>, name: &str) -> usize {323
if let Some(idx) = list.iter().position(|v| v["name"] == name) {324
idx325
} else {326
list.push(interface(name));327
list.len() - 1328
}329
}331
fn text_interfaces(text: &str, list: &mut Vec<Value>) {332
let mut current = None;333
for line in text.lines() {334
let w: Vec<_> = line.split_whitespace().collect();335
if w.len() >= 2 && w[0].trim_end_matches(':').parse::<u32>().is_ok() {336
let named = w[1].trim_end_matches(':');337
let (name, parent) = named338
.split_once('@')339
.map_or((named, None), |(n, p)| (n, Some(p)));340
let idx = ensure(list, name);341
current = Some(idx);342
let v = &mut list[idx];343
if let Some(p) = parent {344
v["parent"] = json!(p);345
}346
if let Some(mtu) = after(&w, "mtu") {347
v["mtu"] = number(mtu);348
}349
if let Some(master) = after(&w, "master") {350
v["master"] = json!(master);351
}352
if let Some(state) = after(&w, "state") {353
v["state"] = json!(match state {354
"UP" => "up",355
"DOWN" => "down",356
_ => "unknown",357
});358
}359
if line.contains("LOOPBACK") {360
v["kind"] = json!("loopback");361
}362
} else if let Some(idx) = current {363
let v = &mut list[idx];364
match w.first().copied().unwrap_or("") {365
"link/ether" | "link/loopback" if w.len() > 1 => {366
v["mac"] = json!(w[1]);367
if v["kind"] == "other" {368
v["kind"] = json!("ethernet");369
}370
}371
"inet" | "inet6" if w.len() > 1 => {372
if let Some((addr, prefix)) = w[1].split_once('/') {373
let a = json!({"family": if w[0] == "inet" {4} else {6}, "address": addr, "prefix": number(prefix),374
"scope": after(&w, "scope"), "dynamic": w.contains(&"dynamic"), "secondary": w.contains(&"secondary"),375
"temporary": w.contains(&"temporary"), "lifetimeValid": null});376
if let Some(addrs) = v["addresses"].as_array_mut() {377
if !addrs.iter().any(|old| old["address"] == addr) {378
addrs.push(a);379
}380
}381
}382
}383
"valid_lft" if w.len() > 1 => {384
if let Some(a) = v["addresses"].as_array_mut().and_then(|a| a.last_mut()) {385
a["lifetimeValid"] = if w[1] == "forever" {386
json!("forever")387
} else {388
number(w[1].trim_end_matches("sec"))389
};390
}391
}392
_ => {}393
}394
}395
}396
}398
fn routes(parts: &[(String, String)]) -> Vec<Value> {399
let mut result = Vec::new();400
for family in [4, 6] {401
let j = body(parts, &format!("route{}-json", family));402
if let Ok(Value::Array(list)) = serde_json::from_str::<Value>(j) {403
for v in list {404
if !v.is_object() {405
continue;406
}407
let dst = v["dst"].as_str();408
result.push(json!({"family": family, "destination": dst, "gateway": v["gateway"], "device": v["dev"],409
"metric": v["metric"], "protocol": v["protocol"], "scope": v["scope"],410
"default": matches!(dst, Some("default" | "0.0.0.0/0" | "::/0"))}));411
}412
} else {413
for line in body(parts, &format!("route{}-text", family)).lines() {414
let w: Vec<_> = line.split_whitespace().collect();415
if w.is_empty() || (!w.contains(&"dev") && !w.contains(&"via")) {416
continue;417
}418
result.push(json!({"family": family, "destination": w[0], "gateway": after(&w, "via"), "device": after(&w, "dev"),419
"metric": after(&w, "metric").map(number), "protocol": after(&w, "proto"), "scope": after(&w, "scope"),420
"default": matches!(w[0], "default" | "0.0.0.0/0" | "::/0")}));421
}422
}423
}424
result425
}427
#[derive(Default, Debug)]428
struct Config {429
name: String,430
mac: String,431
path: String,432
manager: String,433
renderer: Option<String>,434
v4: Option<String>,435
v6: Option<String>,436
dns: Vec<String>,437
kind: Option<String>,438
parent: Option<String>,439
vlan: Option<u64>,440
members: Vec<String>,441
priority: u8,442
}444
fn yaml_entries(text: &str) -> Vec<(Vec<String>, String)> {445
let mut result = Vec::new();446
let mut stack: Vec<(usize, String)> = Vec::new();447
for line in text.lines() {448
let t = line.trim();449
if t.is_empty() || t.starts_with('#') {450
continue;451
}452
let indent = line.len() - line.trim_start().len();453
if let Some(value) = t.strip_prefix("- ") {454
result.push((455
stack.iter().map(|(_, k)| k.clone()).collect(),456
clean(value).to_owned(),457
));458
continue;459
}460
let Some((key, value)) = t.split_once(':') else {461
continue;462
};463
while stack.last().is_some_and(|(i, _)| *i >= indent) {464
stack.pop();465
}466
let mut path: Vec<_> = stack.iter().map(|(_, k)| k.clone()).collect();467
path.push(clean(key).to_owned());468
let value = clean(value.split(" #").next().unwrap_or(""));469
result.push((path, value.to_owned()));470
if value.is_empty() {471
stack.push((indent, clean(key).to_owned()));472
}473
}474
result475
}477
fn yaml_configs(path: &str, text: &str) -> Vec<Config> {478
let entries = yaml_entries(text);479
let mut result = Vec::new();480
let renderer = entries481
.iter()482
.find(|(p, _)| p == &["network", "renderer"])483
.map(|(_, v)| v.clone());484
for (base, _) in &entries {485
if base.len() != 3 || base[0] != "network" {486
continue;487
}488
let cat = base[1].as_str();489
if !matches!(490
cat,491
"ethernets" | "bridges" | "bonds" | "vlans" | "wifis" | "tunnels" | "dummy-devices"492
) || base[2] == "renderer"493
{494
continue;495
}496
let get = |suffix: &[&str]| -> Option<&str> {497
entries498
.iter()499
.find(|(p, _)| {500
p.len() == base.len() + suffix.len()501
&& p.starts_with(base)502
&& p[base.len()..]503
.iter()504
.map(String::as_str)505
.eq(suffix.iter().copied())506
})507
.map(|(_, v)| v.as_str())508
};509
let list = |suffix: &[&str]| -> Vec<String> {510
entries511
.iter()512
.filter(|(p, _)| {513
p.len() == base.len() + suffix.len()514
&& p.starts_with(base)515
&& p[base.len()..]516
.iter()517
.map(String::as_str)518
.eq(suffix.iter().copied())519
})520
.flat_map(|(_, v)| yaml_values(v))521
.collect()522
};523
let mut c = Config {524
name: get(&["set-name"]).unwrap_or(&base[2]).to_owned(),525
mac: get(&["match", "macaddress"]).unwrap_or("").to_owned(),526
path: path.to_owned(),527
manager: "netplan".into(),528
renderer: get(&["renderer"])529
.map(str::to_owned)530
.or_else(|| renderer.clone()),531
priority: 30,532
..Config::default()533
};534
if let Some(name) = get(&["match", "name"]) {535
if get(&["set-name"]).is_none() {536
c.name = name.into();537
}538
}539
// A definition ID with a MAC match is not necessarily a kernel interface name.540
if !c.mac.is_empty() && get(&["set-name"]).is_none() && get(&["match", "name"]).is_none() {541
c.name.clear();542
}543
c.v4 = match get(&["dhcp4"]) {544
Some("true" | "yes") => Some("dhcp".into()),545
Some("false" | "no") => Some("disabled".into()),546
_ => None,547
};548
let dhcp6 = matches!(get(&["dhcp6"]), Some("true" | "yes"));549
let ra = get(&["accept-ra"]);550
c.v6 = if dhcp6 {551
Some(552
if ra == Some("false") {553
"dhcpv6"554
} else {555
"slaac-dhcpv6"556
}557
.into(),558
)559
} else if ra == Some("true") {560
Some("slaac".into())561
} else if get(&["dhcp6"]) == Some("false") && ra == Some("false") {562
Some("disabled".into())563
} else {564
None565
};566
for a in list(&["addresses"]) {567
set_static(&mut c, &a);568
}569
c.dns = list(&["nameservers", "addresses"]);570
c.kind = match cat {571
"bridges" => Some("bridge"),572
"bonds" => Some("bond"),573
"vlans" => Some("vlan"),574
"wifis" => Some("wifi"),575
"dummy-devices" => Some("dummy"),576
_ => None,577
}578
.map(str::to_owned);579
c.parent = get(&["link"]).map(str::to_owned);580
c.vlan = get(&["id"]).and_then(|s| s.parse().ok());581
c.members = list(&["interfaces"]);582
result.push(c);583
}584
result585
}587
fn yaml_values(s: &str) -> Vec<String> {588
s.trim_matches(['[', ']'])589
.split(',')590
.map(clean)591
.filter(|s| !s.is_empty())592
.map(str::to_owned)593
.collect()594
}595
fn set_static(c: &mut Config, a: &str) {596
if let Ok(ip) = a.split('/').next().unwrap_or("").parse::<IpAddr>() {597
if ip.is_ipv4() {598
if c.v4.as_deref() != Some("dhcp") {599
c.v4 = Some("static".into());600
}601
} else if !matches!(c.v6.as_deref(), Some("dhcpv6" | "slaac" | "slaac-dhcpv6")) {602
c.v6 = Some("static".into());603
}604
}605
}607
fn ifup_configs(path: &str, text: &str, manager: &str) -> Vec<Config> {608
let mut result: Vec<Config> = Vec::new();609
for l in text.lines().map(str::trim).filter(|l| !l.starts_with('#')) {610
let w: Vec<_> = l.split_whitespace().collect();611
if w.len() >= 4 && w[0] == "iface" {612
let method = match w[3] {613
"dhcp" if w[2] == "inet6" => "dhcpv6",614
"dhcp" => "dhcp",615
"static" | "loopback" => "static",616
"auto" if w[2] == "inet6" => "slaac",617
_ => "unknown",618
};619
let mut c = Config {620
name: w[1].into(),621
path: path.into(),622
manager: manager.into(),623
priority: if w[3] == "manual" { 5 } else { 25 },624
..Config::default()625
};626
if w[2] == "inet6" {627
c.v6 = (method != "unknown").then(|| method.into());628
} else {629
c.v4 = (method != "unknown").then(|| method.into());630
}631
result.push(c);632
} else if let Some(c) = result.last_mut() {633
if w.len() < 2 {634
continue;635
}636
match w[0] {637
"dns-nameservers" => c.dns.extend(w[1..].iter().map(|s| s.to_string())),638
"bridge-ports" => {639
c.kind = Some("bridge".into());640
c.members = w[1..].iter().map(|s| s.to_string()).collect();641
}642
"bond-slaves" => {643
c.kind = Some("bond".into());644
c.members = w[1..].iter().map(|s| s.to_string()).collect();645
}646
"vlan-raw-device" => {647
c.kind = Some("vlan".into());648
c.parent = Some(w[1].into());649
}650
_ => {}651
}652
}653
}654
result655
}657
fn configs(parts: &[(String, String)], ifup: &str) -> Vec<Config> {658
let mut result = Vec::new();659
for (section, text) in parts {660
let Some(path) = section.strip_prefix("file:") else {661
continue;662
};663
if path.starts_with("/etc/netplan/") {664
result.extend(yaml_configs(path, text));665
} else if path == "/etc/network/interfaces"666
|| path.starts_with("/etc/network/interfaces.d/")667
{668
result.extend(ifup_configs(path, text, ifup));669
} else if path.ends_with(".netdev") {670
let m = ini(text);671
let kind = item(&m, "NetDev.Kind");672
result.push(Config {673
name: item(&m, "NetDev.Name").into(),674
path: path.into(),675
manager: "networkd".into(),676
priority: 1,677
kind: matches!(678
kind,679
"bridge" | "bond" | "vlan" | "wireguard" | "tun" | "veth" | "dummy"680
)681
.then(|| kind.to_owned()),682
vlan: item(&m, "VLAN.Id").parse().ok(),683
..Config::default()684
});685
} else if path.ends_with(".network") {686
let m = ini(text);687
let mut c = Config {688
name: item(&m, "Match.Name").into(),689
mac: item(&m, "Match.MACAddress").into(),690
path: path.into(),691
manager: "networkd".into(),692
priority: 20,693
..Config::default()694
};695
if c.mac.is_empty() {696
c.mac = item(&m, "Match.PermanentMACAddress").into();697
}698
let dhcp = item(&m, "Network.DHCP");699
c.v4 = match dhcp {700
"yes" | "true" | "ipv4" => Some("dhcp".into()),701
"no" | "false" | "ipv6" => Some("disabled".into()),702
_ => None,703
};704
let ra = item(&m, "Network.IPv6AcceptRA");705
c.v6 = if matches!(dhcp, "yes" | "true" | "ipv6") {706
Some(707
if matches!(ra, "no" | "false") {708
"dhcpv6"709
} else {710
"slaac-dhcpv6"711
}712
.into(),713
)714
} else if matches!(ra, "yes" | "true") {715
Some("slaac".into())716
} else if matches!(ra, "no" | "false") && matches!(dhcp, "no" | "false" | "ipv4") {717
Some("disabled".into())718
} else {719
None720
};721
for a in words(item(&m, "Network.Address"))722
.into_iter()723
.chain(words(item(&m, "Address.Address")))724
{725
set_static(&mut c, &a);726
}727
c.dns = words(item(&m, "Network.DNS"));728
result.push(c);729
} else if path.ends_with(".nmconnection") {730
let m = ini(text);731
if item(&m, ".nmmeta.external") == "true" {732
continue;733
}734
let mut name = item(&m, "connection.interface-name").to_owned();735
if name.is_empty() {736
for l in body(parts, "nm-connections").lines() {737
let w: Vec<_> = l.split(':').collect();738
if w.len() >= 5 && w[4] == path {739
name = w[3].into();740
break;741
}742
}743
}744
let mut c = Config {745
name,746
mac: item(&m, "ethernet.mac-address").into(),747
path: path.into(),748
manager: "networkmanager".into(),749
priority: 25,750
..Config::default()751
};752
c.v4 = nm_method(item(&m, "ipv4.method"), false);753
c.v6 = nm_method(item(&m, "ipv6.method"), true);754
c.dns = words(item(&m, "ipv4.dns"));755
c.dns.extend(words(item(&m, "ipv6.dns")));756
if item(&m, "connection.type") == "wifi"757
|| item(&m, "connection.type") == "802-11-wireless"758
{759
c.kind = Some("wifi".into());760
}761
result.push(c);762
}763
}764
let u = if !body(parts, "uci-network").is_empty() {765
uci(body(parts, "uci-network"))766
} else {767
uci(body(parts, "file:/etc/config/network"))768
};769
for b in u {770
let m = &b.fields;771
let mut c = Config {772
name: item(m, if b.kind == "device" { "name" } else { "device" }).into(),773
path: "/etc/config/network".into(),774
manager: "netifd".into(),775
priority: 25,776
..Config::default()777
};778
if c.name.is_empty() {779
c.name = item(m, "ifname").into();780
}781
if b.kind == "interface" {782
c.v4 = match item(m, "proto") {783
"dhcp" => Some("dhcp".into()),784
"static" => Some("static".into()),785
"none" => Some("disabled".into()),786
_ => None,787
};788
c.v6 = match item(m, "proto") {789
"dhcpv6" => Some("dhcpv6".into()),790
_ => None,791
};792
c.dns = words(item(m, "dns"));793
if !item(m, "ip6addr").is_empty() {794
c.v6 = Some("static".into());795
}796
if item(m, "ipv6") == "0" {797
c.v6 = Some("disabled".into());798
}799
} else if b.kind == "device" {800
c.kind = match item(m, "type") {801
"bridge" => Some("bridge".into()),802
"8021q" | "8021ad" => Some("vlan".into()),803
_ => None,804
};805
c.members = words(item(m, "ports"));806
c.parent = m.get("ifname").cloned();807
c.vlan = item(m, "vid").parse().ok();808
c.priority = 10;809
} else {810
continue;811
}812
result.push(c);813
}814
result815
}817
fn nm_method(method: &str, v6: bool) -> Option<String> {818
match method {819
"auto" => Some(if v6 { "slaac-dhcpv6" } else { "dhcp" }.into()),820
"manual" => Some("static".into()),821
"disabled" => Some("disabled".into()),822
"dhcp" if v6 => Some("dhcpv6".into()),823
_ => None,824
}825
}827
fn matches(c: &Config, iface: &Value) -> bool {828
let name = iface["name"].as_str().unwrap_or("");829
let mac = iface["mac"].as_str().unwrap_or("");830
if c.name.is_empty() && c.mac.is_empty() {831
return false;832
}833
(c.name.is_empty() || c.name.split_whitespace().any(|p| glob(p, name)))834
&& (c.mac.is_empty() || c.mac.eq_ignore_ascii_case(mac))835
}837
fn glob(pattern: &str, text: &str) -> bool {838
if pattern == "*" {839
return true;840
}841
if let Some((a, b)) = pattern.split_once('*') {842
text.starts_with(a) && text.ends_with(b)843
} else {844
pattern == text845
}846
}848
fn manager_info(parts: &[(String, String)], configs: &[Config], ifup: &str) -> Value {849
let mut managers = Vec::new();850
let services = body(parts, "services");851
for (name, service) in [852
("networkd", "systemd-networkd"),853
("networkmanager", "NetworkManager"),854
(ifup, "networking"),855
("netifd", "network"),856
] {857
let evidence: Vec<_> = services858
.lines()859
.filter(|l| {860
let w: Vec<_> = l.split_whitespace().collect();861
w.len() >= 2 && w[0] == service && w[1] == "active"862
})863
.map(|l| json!({"section": "services", "value": l}))864
.collect();865
if !evidence.is_empty() {866
managers.push(json!({"name": name, "evidence": evidence}));867
}868
}869
let has_plan = parts870
.iter()871
.any(|(s, _)| s.starts_with("file:/etc/netplan/"));872
let tools = body(parts, "tools");873
if has_plan874
&& tools.lines().any(|l| l == "netplan")875
&& managers876
.iter()877
.any(|m| m["name"] == "networkd" || m["name"] == "networkmanager")878
{879
managers.insert(0, json!({"name": "netplan", "evidence": [{"section": "tools", "value": "netplan"}, {"section": "files", "value": "/etc/netplan/"}]}));880
}881
let primary = if managers.iter().any(|m| m["name"] == ifup) && ifup == "ifupdown2" {882
ifup883
} else {884
managers885
.first()886
.and_then(|m| m["name"].as_str())887
.unwrap_or("unknown")888
};889
let renderer = if has_plan {890
configs.iter().find_map(|c| c.renderer.clone()).or_else(|| {891
if managers.iter().any(|m| m["name"] == "networkd") {892
Some("networkd".into())893
} else if managers.iter().any(|m| m["name"] == "networkmanager") {894
Some("NetworkManager".into())895
} else {896
None897
}898
})899
} else {900
None901
};902
let mut files = Vec::new();903
let mut seen = BTreeSet::new();904
for (s, _) in parts {905
if let Some(path) = s.strip_prefix("file:") {906
let kind = if path.starts_with("/etc/netplan/") {907
"netplan"908
} else if path.ends_with(".network") || path.ends_with(".netdev") {909
"networkd"910
} else if path.contains("NetworkManager/") {911
"networkmanager"912
} else if path.starts_with("/etc/network/interfaces") {913
ifup914
} else if path == "/etc/config/network" {915
"uci"916
} else if path.contains("/ifcfg-") {917
"ifcfg"918
} else {919
continue;920
};921
if seen.insert(path) {922
files.push(json!({"path": path, "kind": kind}));923
}924
}925
}926
json!({"manager": primary, "renderer": renderer, "managers": managers, "files": files})927
}929
fn apply_configs(930
ifaces: &mut [Value],931
configs: &[Config],932
routes: &[Value],933
info: &Value,934
parts: &[(String, String)],935
) {936
for iface in ifaces.iter_mut() {937
let mut selected: Vec<_> = configs.iter().filter(|c| matches(c, iface)).collect();938
selected.sort_by_key(|c| std::cmp::Reverse(c.priority));939
if let Some(c) = selected.first() {940
iface["configuredIn"] = json!(c.path);941
iface["managedBy"] = json!(c.manager);942
let mut source = None;943
for c in &selected {944
if iface["ipv4Method"] == "unknown" {945
if let Some(v) = &c.v4 {946
iface["ipv4Method"] = json!(v);947
source.get_or_insert(&c.path);948
}949
}950
if iface["ipv6Method"] == "unknown" {951
if let Some(v) = &c.v6 {952
iface["ipv6Method"] = json!(v);953
source.get_or_insert(&c.path);954
}955
}956
if let Some(k) = &c.kind {957
iface["kind"] = json!(k);958
}959
if let Some(p) = &c.parent {960
iface["parent"] = json!(p);961
}962
if let Some(v) = c.vlan {963
iface["vlanId"] = json!(v);964
}965
}966
iface["methodSource"] = json!(source);967
let dns: BTreeSet<_> = selected968
.iter()969
.flat_map(|c| c.dns.iter().cloned())970
.collect();971
iface["dns"] = json!(dns);972
} else {973
let name = iface["name"].as_str().unwrap_or("");974
if body(parts, "nm-devices").lines().any(|l| {975
l.starts_with(&format!("{}:", name))976
&& !l.contains(":unmanaged:")977
&& !l.contains("(externally)")978
}) {979
iface["managedBy"] = json!("networkmanager");980
} else if body(parts, "networkd").lines().any(|l| {981
let w: Vec<_> = l.split_whitespace().collect();982
w.len() > 4 && w[1] == name && w[4] == "configured"983
}) {984
iface["managedBy"] = json!(if info["manager"] == "netplan" {985
"netplan"986
} else {987
"networkd"988
});989
}990
}991
let name = iface["name"].clone();992
let mut defaults: Vec<_> = routes993
.iter()994
.filter(|r| r["device"] == name && r["default"] == true)995
.collect();996
defaults.sort_by_key(|r| r["metric"].as_u64().unwrap_or(0));997
for family in [4, 6] {998
if let Some(r) = defaults.iter().find(|r| r["family"] == family) {999
iface[if family == 4 { "gateway4" } else { "gateway6" }] = r["gateway"].clone();1000
if iface["metric"].is_null() {1001
iface["metric"] = r["metric"].clone();1002
}1003
}1004
let field = if family == 4 {1005
"ipv4Method"1006
} else {1007
"ipv6Method"1008
};1009
if iface[field] == "unknown" {1010
let addrs = iface["addresses"].as_array().cloned().unwrap_or_default();1011
let global: Vec<_> = addrs1012
.iter()1013
.filter(|a| a["family"] == family && a["scope"] == "global")1014
.collect();1015
let dhcp = routes.iter().any(|r| {1016
r["device"] == name && r["family"] == family && r["protocol"] == "dhcp"1017
});1018
let ra = routes1019
.iter()1020
.any(|r| r["device"] == name && r["family"] == family && r["protocol"] == "ra");1021
let method = if dhcp && ra && family == 6 {1022
"slaac-dhcpv6"1023
} else if dhcp {1024
if family == 4 {1025
"dhcp"1026
} else {1027
"dhcpv6"1028
}1029
} else if family == 6 && ra {1030
"slaac"1031
} else if family == 4 && global.iter().any(|a| a["dynamic"] == true) {1032
"dhcp"1033
} else if !global.is_empty() && global.iter().all(|a| a["dynamic"] == false) {1034
"static"1035
} else {1036
"unknown"1037
};1038
iface[field] = json!(method);1039
}1040
}1041
}1042
for c in configs {1043
for member in &c.members {1044
if let Some(v) = ifaces.iter_mut().find(|v| v["name"] == *member) {1045
v["master"] = json!(c.name);1046
if v["managedBy"].is_null() {1047
v["managedBy"] = json!(c.manager);1048
}1049
}1050
}1051
}1052
}1054
fn dns(parts: &[(String, String)], ifaces: &mut [Value]) -> Value {1055
let mut servers = Vec::<String>::new();1056
let mut search = Vec::<String>::new();1057
let mut link_name = None;1058
let mut continuation = false;1059
for line in body(parts, "resolved").lines() {1060
let t = line.trim();1061
if t.starts_with("Link ") {1062
link_name = t1063
.split_once('(')1064
.and_then(|(_, r)| r.split_once(')'))1065
.map(|(n, _)| n.to_owned());1066
}1067
let (is_dns, values) = if let Some((key, value)) = t1068
.split_once(": ")1069
.or_else(|| t.strip_suffix(':').map(|k| (k, "")))1070
{1071
continuation = key.trim() == "DNS Servers";1072
(continuation, value.trim())1073
} else {1074
(continuation && !t.is_empty(), t)1075
};1076
if is_dns {1077
for token in values.split_whitespace() {1078
let addr = token.split('#').next().unwrap_or("");1079
if addr.parse::<IpAddr>().is_err() {1080
continue;1081
}1082
push_unique(&mut servers, addr);1083
if let Some(iface) = ifaces1084
.iter_mut()1085
.find(|i| Some(i["name"].as_str().unwrap_or("")) == link_name.as_deref())1086
{1087
if let Some(a) = iface["dns"].as_array_mut() {1088
if !a.iter().any(|v| v == addr) {1089
a.push(json!(addr));1090
}1091
}1092
}1093
}1094
}1095
if let Some((key, value)) = t.split_once(": ") {1096
if key.trim() == "DNS Domain" {1097
for s in value.split_whitespace() {1098
if s != "." {1099
push_unique(&mut search, s);1100
}1101
}1102
}1103
}1104
}1105
let resolved = !servers.is_empty();1106
for line in body(parts, "file:/etc/resolv.conf").lines().map(str::trim) {1107
let w: Vec<_> = line.split_whitespace().collect();1108
if w.len() >= 2 && w[0] == "nameserver" && !resolved {1109
if w[1].parse::<IpAddr>().is_ok() {1110
push_unique(&mut servers, w[1]);1111
}1112
}1113
if w.len() >= 2 && matches!(w[0], "search" | "domain") {1114
for s in &w[1..] {1115
if *s != "." {1116
push_unique(&mut search, s);1117
}1118
}1119
}1120
}1121
json!({"servers": servers, "search": search, "source": if resolved { Some("resolved") } else if !body(parts, "file:/etc/resolv.conf").is_empty() { Some("/etc/resolv.conf") } else { None }})1122
}1123
fn push_unique(v: &mut Vec<String>, s: &str) {1124
if !v.iter().any(|x| x == s) {1125
v.push(s.to_owned());1126
}1127
}1129
fn listening(text: &str) -> Vec<Value> {1130
text.lines().filter_map(|l| {1131
let w: Vec<_> = l.split_whitespace().collect();1132
if w.len() < 5 || !matches!(w[0], "tcp" | "udp" | "tcp6" | "udp6") { return None; }1133
// netstat has the queues right after the protocol and the address one column earlier than ss.1134
let netstat = w[1].parse::<u32>().is_ok();1135
let (addr, port) = w[if netstat { 3 } else { 4 }].rsplit_once(':')?;1136
let port = port.parse::<u16>().ok()?;1137
Some(json!({"protocol": w[0], "address": addr.trim_matches(['[', ']']).split('%').next(), "port": port, "processes": listening_processes(l, netstat)}))1138
}).collect()1139
}1141
/// Only root sees the programs of other users, so this may well be empty.1142
fn listening_processes(line: &str, netstat: bool) -> Vec<String> {1143
let mut names: Vec<String> = Vec::new();1144
if netstat {1145
// "2654/sshd [listener] 0 of 10" on BusyBox, and the name may end in a colon.1146
let owner = line.split_whitespace().filter_map(|word| word.split_once('/')).find(|(pid, name)| !pid.is_empty() && !name.is_empty() && pid.bytes().all(|byte| byte.is_ascii_digit()));1147
if let Some((_, name)) = owner {1148
names.push(name.trim_end_matches(':').to_string());1149
}1150
} else if let Some((_, users)) = line.split_once("users:(") {1151
for part in users.split("(\"").skip(1) {1152
if let Some((name, _)) = part.split_once('"') {1153
if !names.iter().any(|known| known == name) {1154
names.push(name.to_string());1155
}1156
}1157
}1158
}1159
names1160
}1162
fn ssh(text: &str, ifaces: &mut [Value]) -> Value {1163
let w: Vec<_> = text.split_whitespace().collect();1164
let mut result = json!({"clientAddress": null, "clientPort": null, "serverAddress": null, "serverPort": null, "family": null, "interface": null});1165
if w.len() != 4 {1166
return result;1167
}1168
let (Ok(client), Ok(cport), Ok(server), Ok(sport)) = (1169
w[0].parse::<IpAddr>(),1170
w[1].parse::<u16>(),1171
w[2].parse::<IpAddr>(),1172
w[3].parse::<u16>(),1173
) else {1174
return result;1175
};1176
result["clientAddress"] = json!(client.to_string());1177
result["clientPort"] = json!(cport);1178
result["serverAddress"] = json!(server.to_string());1179
result["serverPort"] = json!(sport);1180
result["family"] = json!(if server.is_ipv4() { 4 } else { 6 });1181
for iface in ifaces {1182
if iface["addresses"].as_array().is_some_and(|a| {1183
a.iter().any(|a| {1184
a["address"].as_str().and_then(|s| s.parse::<IpAddr>().ok()) == Some(server)1185
})1186
}) {1187
iface["ssh"] = json!(true);1188
result["interface"] = iface["name"].clone();1189
break;1190
}1191
}1192
result1193
}1195
fn inventory_findings(1196
config: &Value,1197
ifaces: &[Value],1198
routes: &[Value],1199
firewall: &Value,1200
) -> Vec<Value> {1201
let mut findings = Vec::new();1202
// Netplan and its renderer are one management stack, not competing managers.1203
let managers = config["managers"].as_array().cloned().unwrap_or_default();1204
let has_plan = managers.iter().any(|m| m["name"] == "netplan");1205
let renderer = match config["renderer"].as_str() {1206
Some("networkd") => "networkd",1207
Some("NetworkManager") => "networkmanager",1208
_ => "",1209
};1210
let independent = managers1211
.iter()1212
.filter(|m| !(has_plan && m["name"] == renderer))1213
.count();1214
if independent > 1 {1215
findings.push(finding(1216
"warning",1217
"several-network-managers",1218
json!({"managers": managers.iter().map(|m| m["name"].clone()).collect::<Vec<_>>()}),1219
));1220
}1221
if !routes1222
.iter()1223
.any(|r| r["family"] == 4 && r["default"] == true)1224
{1225
findings.push(finding("warning", "no-default-route", json!({})));1226
}1227
if ifaces.iter().any(|i| {1228
i["addresses"]1229
.as_array()1230
.is_some_and(|a| a.iter().any(|a| a["family"] == 6 && a["scope"] == "global"))1231
}) && !routes1232
.iter()1233
.any(|r| r["family"] == 6 && r["default"] == true)1234
{1235
findings.push(finding("warning", "no-default-route-v6", json!({})));1236
}1237
for iface in ifaces {1238
if iface["carrier"] == false && iface["kind"] != "loopback" {1239
findings.push(finding(1240
"warning",1241
"interface-no-carrier",1242
json!({"interface": iface["name"]}),1243
));1244
}1245
}1246
if firewall["readable"] == false {1247
findings.push(finding(1248
"warning",1249
"needs-root",1250
json!({"component": "firewall"}),1251
));1252
}1253
if let Some(f) = firewall["findings"].as_array() {1254
findings.extend(f.iter().cloned());1255
}1256
findings1257
}1259
pub fn parse(stdout: &str) -> Value {1260
let parts = sections(stdout);1261
let system = pairs(body(&parts, "system"));1262
let uid = item(&system, "uid").parse::<u64>().ok();1263
let tools = body(&parts, "tools");1264
let ifup = if tools.lines().any(|s| s == "ifreload") {1265
"ifupdown2"1266
} else if tools.lines().any(|s| s == "ifupdown-ng")1267
|| parts.iter().any(|(_, s)| s.contains("ifupdown-ng"))1268
{1269
"ifupdown-ng"1270
} else {1271
"ifupdown"1272
};1273
let configs = configs(&parts, ifup);1274
let config = manager_info(&parts, &configs, ifup);1275
let mut ifaces = interfaces(&parts);1276
let routes = routes(&parts);1277
apply_configs(&mut ifaces, &configs, &routes, &config, &parts);1278
let dns = dns(&parts, &mut ifaces);1279
let ssh = ssh(item(&system, "ssh"), &mut ifaces);1280
let firewall = super::firewall::parse(1281
&parts,1282
ssh["serverPort"]1283
.as_u64()1284
.and_then(|n| u16::try_from(n).ok()),1285
);1286
let findings = inventory_findings(&config, &ifaces, &routes, &firewall);1287
json!({"system": {"uid": uid, "root": uid.map(|u| u == 0), "hostname": scalar(item(&system, "hostname")), "kernel": scalar(item(&system, "kernel")),1288
"osId": scalar(item(&system, "ID")), "osVersion": scalar(item(&system, "VERSION_ID")), "osName": scalar(item(&system, "PRETTY_NAME"))},1289
"config": config, "interfaces": ifaces, "routes": routes, "dns": dns, "ssh": ssh,1290
"listening": listening(body(&parts, "listening")), "firewall": firewall, "findings": findings})1291
}1293
#[cfg(test)]1294
mod tests {1295
use super::*;1296
fn input(parts: &[(&str, &str)]) -> String {1297
parts1298
.iter()1299
.map(|(n, b)| format!("\x1eCHJ_SECTION={}\n{}\n", n, b))1300
.collect()1301
}1302
fn eth(v: &Value) -> &Value {1303
v["interfaces"]1304
.as_array()1305
.unwrap()1306
.iter()1307
.find(|i| i["name"] == "eth0")1308
.unwrap()1309
}1310
const ADDR: &str = r#"[{"ifindex":2,"ifname":"eth0","mtu":1500,"operstate":"UP","link_type":"ether","address":"52:54:00:00:00:01","addr_info":[{"family":"inet","local":"192.0.2.10","prefixlen":24,"scope":"global","dynamic":true,"valid_life_time":42000},{"family":"inet6","local":"2001:db8::10","prefixlen":64,"scope":"global","temporary":true}]}]"#;1312
#[test]1313
fn section_order_duplicates_and_boundaries() {1314
assert_eq!(1315
sections("noise\n\x1eCHJ_SECTION=a\r\nx\r\n\x1eCHJ_SECTION=a\ny\n\x1eCHJ_SECTION=b"),1316
vec![1317
("a".into(), "x\r\n".into()),1318
("a".into(), "y\n".into()),1319
("b".into(), "".into())1320
]1321
);1322
assert!(sections("x\x1eCHJ_SECTION=a").is_empty());1323
}1324
#[test]1325
fn netplan_precedes_address_flags_and_generated_file() {1326
let s = input(&[("addr-json", ADDR), ("tools", "netplan\nnetworkctl"), ("services", "systemd-networkd active enabled"),1327
("file:/etc/netplan/test.yaml", "network:\n version: 2\n renderer: networkd\n ethernets:\n eth0:\n dhcp4: false\n addresses:\n - 192.0.2.10/24\n - 2001:db8::10/64\n nameservers:\n addresses: [192.0.2.53, '2001:db8::53']"),1328
("file:/run/systemd/network/10-netplan-eth0.network", "[Match]\nName=eth0\n[Network]\nDHCP=ipv4")]);1329
let v = parse(&s);1330
let e = eth(&v);1331
assert_eq!(e["ipv4Method"], "static");1332
assert_eq!(e["ipv6Method"], "static");1333
assert_eq!(e["methodSource"], "/etc/netplan/test.yaml");1334
assert_eq!(e["dns"].as_array().unwrap().len(), 2);1335
assert_eq!(v["config"]["renderer"], "networkd");1336
assert!(!v["findings"]1337
.as_array()1338
.unwrap()1339
.iter()1340
.any(|f| f["code"] == "several-network-managers"));1341
}1342
#[test]1343
fn busybox_text_addresses_routes_and_lifetimes() {1344
let s = input(&[("addr-text", "2: eth0: <BROADCAST,UP,LOWER_UP> mtu 1500 state UP\n link/ether 52:54:00:00:00:01 brd ff:ff:ff:ff:ff:ff\n inet 192.0.2.10/24 scope global secondary dynamic eth0\n valid_lft 300sec preferred_lft 200sec\n inet6 2001:db8::10/64 scope global temporary dynamic\n valid_lft forever preferred_lft forever"),1345
("route4-text", "default via 192.0.2.1 dev eth0 proto dhcp metric 42\n192.0.2.0/24 dev eth0 scope link"),1346
("route6-text", "default via 2001:db8::1 dev eth0 proto ra metric 100"),1347
("sys:eth0", "physical=1\ncarrier=0\nspeed=1000\nduplex=full\nmtu=1500\naddress=52:54:00:00:00:01\noperstate=down")]);1348
let v = parse(&s);1349
let e = eth(&v);1350
assert_eq!(e["ipv4Method"], "dhcp");1351
assert_eq!(e["ipv6Method"], "slaac");1352
assert_eq!(e["gateway4"], "192.0.2.1");1353
assert_eq!(e["metric"], 42);1354
assert_eq!(e["state"], "down");1355
assert_eq!(e["speedMbps"], 1000);1356
assert_eq!(e["addresses"][0]["lifetimeValid"], 300);1357
assert_eq!(e["addresses"][0]["secondary"], true);1358
assert_eq!(e["addresses"][1]["temporary"], true);1359
assert_eq!(e["addresses"][1]["lifetimeValid"], "forever");1360
assert!(v["findings"]1361
.as_array()1362
.unwrap()1363
.iter()1364
.any(|f| f["code"] == "interface-no-carrier"));1365
}1366
#[test]1367
fn ssh_ipv6_and_missing_version() {1368
let v = parse(&input(&[1369
(1370
"system",1371
"uid=1001\nID=arch\nssh=2001:db8::20 12345 2001:db8::10 2222",1372
),1373
("addr-json", ADDR),1374
]));1375
assert_eq!(v["ssh"]["family"], 6);1376
assert_eq!(v["ssh"]["interface"], "eth0");1377
assert_eq!(eth(&v)["ssh"], true);1378
assert!(v["system"]["osVersion"].is_null());1379
assert_eq!(v["system"]["root"], false);1380
}1381
#[test]1382
fn networkd_and_nm_match_mac() {1383
for (path, config, expected) in [1384
("/etc/systemd/network/test.network", "[Match]\nMACAddress=52:54:00:00:00:01\n[Network]\nDHCP=ipv6\nIPv6AcceptRA=no\nAddress=192.0.2.10/24", "networkd"),1385
("/etc/NetworkManager/system-connections/test.nmconnection", "[connection]\ntype=ethernet\n[ethernet]\nmac-address=52:54:00:00:00:01\n[ipv4]\nmethod=manual\n[ipv6]\nmethod=dhcp", "networkmanager"),1386
] {1387
let v = parse(&input(&[("addr-json", ADDR), (&format!("file:{path}"), config)]));1388
assert_eq!(eth(&v)["ipv4Method"], "static"); assert_eq!(eth(&v)["ipv6Method"], "dhcpv6"); assert_eq!(eth(&v)["managedBy"], expected);1389
}1390
}1391
#[test]1392
fn bridge_vlan_and_ifupdown2_ownership() {1393
let v = parse(&input(&[("addr-json", ADDR), ("addr-text", "3: vmbr0: <UP> mtu 1500 state DOWN\n4: eth0.42@eth0: <UP> mtu 1500 state DOWN"),1394
("tools", "ifup\nifreload\nnetplan\nnetworkctl"), ("services", "networking active enabled\nsystemd-networkd active enabled"),1395
("file:/etc/network/interfaces", "iface eth0 inet manual\nauto vmbr0\niface vmbr0 inet static\n address 192.0.2.11/24\n bridge-ports eth0\niface eth0.42 inet manual\n vlan-raw-device eth0"),1396
("file:/etc/netplan/test.yaml", "network:\n ethernets:\n eth0:\n dhcp4: true")]));1397
let list = v["interfaces"].as_array().unwrap();1398
assert_eq!(v["config"]["manager"], "ifupdown2");1399
assert_eq!(eth(&v)["managedBy"], "netplan");1400
assert_eq!(eth(&v)["master"], "vmbr0");1401
let bridge = list.iter().find(|i| i["name"] == "vmbr0").unwrap();1402
assert_eq!(bridge["managedBy"], "ifupdown2");1403
assert_eq!(bridge["kind"], "bridge");1404
assert_eq!(1405
list.iter().find(|i| i["name"] == "eth0.42").unwrap()["parent"],1406
"eth0"1407
);1408
}1409
#[test]1410
fn listening_names_the_programs_from_ss_and_netstat() {1411
let v = parse(&input(&[("addr-json", ADDR), ("listening", "Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process\ntcp LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:((\"nginx\",pid=12,fd=6),(\"nginx\",pid=11,fd=6))\ntcp LISTEN 0 511 *:8080 *:* users:((\"apache2\",pid=20,fd=4),(\"httpd\",pid=21,fd=4))\ntcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:*\ntcp 0 0 0.0.0.0:443 0.0.0.0:* LISTEN 33/nginx\nudp 0 0 :::53 :::* 40/dnsmasq:\ntcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 2654/sshd [listener\ntcp 0 0 0.0.0.0:9 0.0.0.0:* LISTEN -")]));1412
let list = v["listening"].as_array().unwrap();1413
assert_eq!(list[0]["port"], 80);1414
assert_eq!(list[0]["processes"], json!(["nginx"]));1415
assert_eq!(list[1]["processes"], json!(["apache2", "httpd"]));1416
assert_eq!(list[2]["processes"], json!([]));1417
assert_eq!((&list[3]["port"], &list[3]["processes"]), (&json!(443), &json!(["nginx"])));1418
assert_eq!((&list[4]["port"], &list[4]["processes"]), (&json!(53), &json!(["dnsmasq"])));1419
assert_eq!(list[5]["processes"], json!(["sshd"]));1420
assert_eq!(list[6]["processes"], json!([]));1421
}1422
#[test]1423
fn resolved_uses_uplink_and_listening_handles_ipv6() {1424
let v = parse(&input(&[("addr-json", ADDR), ("file:/etc/resolv.conf", "nameserver 127.0.0.53\nsearch example.test"),1425
("resolved", "Link 2 (eth0)\n DNS Servers: 192.0.2.53\n 2001:db8::53\n DNS Domain: example.test"),1426
("listening", "Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port\nudp UNCONN 0 0 192.0.2.10%eth0:68 0.0.0.0:*\ntcp LISTEN 0 128 [::]:2222 [::]:*\ntcp LISTEN 0 128 *:9090 *:*")]));1427
assert_eq!(v["dns"]["servers"], json!(["192.0.2.53", "2001:db8::53"]));1428
assert_eq!(eth(&v)["dns"].as_array().unwrap().len(), 2);1429
assert_eq!(v["listening"][1]["address"], "::");1430
assert_eq!(v["listening"][1]["port"], 2222);1431
}1432
#[test]1433
fn hostile_input_and_nulls() {1434
for s in [1435
"".into(),1436
"garbage".into(),1437
input(&[1438
("addr-json", "[{\"ifname\":"),1439
("link-json", "null"),1440
("route4-json", "[null,3,{}]"),1441
]),1442
"x".repeat(1_000_000),1443
input(&[1444
("addr-text", &"é".repeat(100_000)),1445
("system", "uid=invalid\nssh=broken"),1446
]),1447
] {1448
let v = parse(&s);1449
assert!(v.is_object());1450
assert!(v["interfaces"].is_array());1451
}1452
let v = parse("");1453
assert!(v["system"]["uid"].is_null());1454
assert!(v["ssh"]["interface"].is_null());1455
assert_eq!(v["config"]["manager"], "unknown");1456
}1457
}SHA-256
b93c0ebb181573a688cfe76795a9cda9c000b17801399f62e87f01dfeec9121eCH-J Proprietary Software License 1.14 · Allikas on esitatud CH-J Proprietary Software License 1.14 all. Selle kättesaadavus ei muuda litsentsitingimusi ega anna täiendavaid õigusi.
Arhiiv SHA-256
ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f