Public Access
The plan and decisions Q105 to Q116 (docs/milestones/S1.md). lib/net: strict IPv4 parsing, prefix and mask, and the checks a Fixed setting must pass, each refusal with its reason. A Saved Network is Automatic or Fixed (address/prefix and an optional gateway), kept with it in flash. Settings: two DNS servers (9.9.9.9, 1.1.1.1), "Always use my DNS", two NTP servers (pool.ntp.org, time.cloudflare.com). Host-tested: 383 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
98 lines
4.1 KiB
C++
98 lines
4.1 KiB
C++
#include "ipv4.h"
|
|
|
|
#include <cstdio>
|
|
|
|
namespace roro::net {
|
|
|
|
bool parseIpv4(const std::string& text, uint32_t& out) {
|
|
uint32_t value = 0;
|
|
int parts = 0, digits = 0, part = 0;
|
|
for (char c : text) {
|
|
if (c >= '0' && c <= '9') {
|
|
if (++digits > 3) return false;
|
|
part = part * 10 + (c - '0');
|
|
if (part > 255) return false;
|
|
} else if (c == '.') {
|
|
if (digits == 0 || ++parts > 3) return false;
|
|
value = value << 8 | part;
|
|
part = digits = 0;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
if (digits == 0 || parts != 3) return false;
|
|
out = value << 8 | part;
|
|
return true;
|
|
}
|
|
|
|
std::string formatIpv4(uint32_t a) {
|
|
char s[16];
|
|
std::snprintf(s, sizeof s, "%u.%u.%u.%u", static_cast<unsigned>(a >> 24), static_cast<unsigned>(a >> 16 & 255),
|
|
static_cast<unsigned>(a >> 8 & 255), static_cast<unsigned>(a & 255));
|
|
return s;
|
|
}
|
|
|
|
uint32_t maskOf(int prefix) { return prefix <= 0 ? 0 : prefix >= 32 ? 0xFFFFFFFFu : ~0u << (32 - prefix); }
|
|
|
|
std::string checkFixed(const FixedIp& f) {
|
|
if (f.prefix < 1 || f.prefix > 30) return "The prefix must be 1 to 30";
|
|
if (f.address == 0) return "0.0.0.0 isn't an address a device can have";
|
|
uint32_t mask = maskOf(f.prefix), network = f.address & mask, broadcast = network | ~mask;
|
|
if (f.address == network) return formatIpv4(f.address) + " is the network's own address";
|
|
if (f.address == broadcast) return formatIpv4(f.address) + " is the broadcast address";
|
|
if (f.gateway == 0) return "";
|
|
if (f.gateway == f.address) return "The gateway can't be this device's address";
|
|
if ((f.gateway & mask) != network)
|
|
return "The gateway " + formatIpv4(f.gateway) + " isn't in " + formatIpv4(network) + "/" + std::to_string(f.prefix);
|
|
if (f.gateway == broadcast) return "The gateway " + formatIpv4(f.gateway) + " is the broadcast address";
|
|
if (f.gateway == network) return "The gateway " + formatIpv4(f.gateway) + " is the network's own address";
|
|
return "";
|
|
}
|
|
|
|
std::string parseFixed(const std::string& text, FixedIp& out) {
|
|
size_t slash = text.find('/');
|
|
if (slash == std::string::npos) return "Write it as address/prefix, then the gateway if there is one";
|
|
size_t space = text.find(' ', slash);
|
|
std::string address = text.substr(0, slash);
|
|
std::string prefix = text.substr(slash + 1, space == std::string::npos ? std::string::npos : space - slash - 1);
|
|
std::string gateway = space == std::string::npos ? "" : text.substr(space + 1);
|
|
FixedIp f;
|
|
if (!parseIpv4(address, f.address)) return address + " isn't an IPv4 address";
|
|
int p = 0;
|
|
if (prefix.empty() || prefix.size() > 2) return "The prefix must be 1 to 30";
|
|
for (char c : prefix) {
|
|
if (c < '0' || c > '9') return "The prefix must be 1 to 30";
|
|
p = p * 10 + (c - '0');
|
|
}
|
|
f.prefix = static_cast<uint8_t>(p);
|
|
if (!gateway.empty() && !parseIpv4(gateway, f.gateway)) return gateway + " isn't an IPv4 address";
|
|
std::string why = checkFixed(f);
|
|
if (why.empty()) out = f;
|
|
return why;
|
|
}
|
|
|
|
std::string formatFixed(const FixedIp& f) {
|
|
std::string s = formatIpv4(f.address) + "/" + std::to_string(f.prefix);
|
|
if (f.gateway) s += " " + formatIpv4(f.gateway);
|
|
return s;
|
|
}
|
|
|
|
bool validHost(const std::string& text) {
|
|
if (text.empty() || text.size() > 63) return false;
|
|
uint32_t ignored;
|
|
if (parseIpv4(text, ignored)) return true;
|
|
bool allNumeric = true; // digits and dots only, but not an address: "1.2.3", "999.1.1.1"
|
|
char previous = '.';
|
|
for (char c : text) {
|
|
bool letter = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'), digit = c >= '0' && c <= '9';
|
|
if (!letter && !digit && c != '-' && c != '.') return false;
|
|
if (c == '.' && (previous == '.' || previous == '-')) return false; // empty label, or one ending in '-'
|
|
if (c == '-' && previous == '.') return false; // a label starting with '-'
|
|
if (letter || c == '-') allNumeric = false;
|
|
previous = c;
|
|
}
|
|
return previous != '.' && previous != '-' && !allNumeric;
|
|
}
|
|
|
|
} // namespace roro::net
|