Allow raw ip addresses
This commit is contained in:
@@ -82,6 +82,13 @@ For each check:
|
|||||||
At no point is the container's own (router-provided) DNS resolver
|
At no point is the container's own (router-provided) DNS resolver
|
||||||
consulted for endpoint hostnames.
|
consulted for endpoint hostnames.
|
||||||
|
|
||||||
|
**Literal IPs in `host`:** if `host` is already an IPv4/IPv6 address (e.g.
|
||||||
|
`192.168.0.2`), DNS resolution is skipped entirely — the monitor connects
|
||||||
|
to it directly, and `dns_server` is optional/ignored for that endpoint.
|
||||||
|
(Without this, a literal IP would be sent as a DNS query to whatever
|
||||||
|
`dns_server` you configured, which returns NXDOMAIN — it is not a valid
|
||||||
|
hostname — and the check would always fail.)
|
||||||
|
|
||||||
## What gets written to InfluxDB
|
## What gets written to InfluxDB
|
||||||
|
|
||||||
Measurement `uptime_check`, one point per endpoint per check:
|
Measurement `uptime_check`, one point per endpoint per check:
|
||||||
|
|||||||
+19
-11
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
@@ -32,19 +33,26 @@ def check_endpoint(ep, default_timeout: float, timestamp_ns: int) -> CheckResult
|
|||||||
timeout = ep.timeout_seconds or default_timeout
|
timeout = ep.timeout_seconds or default_timeout
|
||||||
start = time.monotonic()
|
start = time.monotonic()
|
||||||
|
|
||||||
try:
|
if dns_resolver.is_ip_literal(ep.host):
|
||||||
resolved_ip = dns_resolver.resolve(ep.host, ep.dns_server, timeout)
|
# Nothing to resolve — connect straight to the given IP. dns_server
|
||||||
except dns.exception.DNSException as e:
|
# is not required/used in this case (see config.py).
|
||||||
return CheckResult(
|
resolved_ip = ep.host
|
||||||
endpoint_name=ep.name, host=ep.host, scheme=ep.scheme, port=ep.port,
|
pin = contextlib.nullcontext()
|
||||||
success=False, status_code=None, expected_status=ep.expected_status,
|
else:
|
||||||
response_time_ms=None, resolved_ip=None,
|
try:
|
||||||
error=f"dns_error: {e}", timestamp_ns=timestamp_ns,
|
resolved_ip = dns_resolver.resolve(ep.host, ep.dns_server, timeout)
|
||||||
)
|
except dns.exception.DNSException as e:
|
||||||
|
return CheckResult(
|
||||||
|
endpoint_name=ep.name, host=ep.host, scheme=ep.scheme, port=ep.port,
|
||||||
|
success=False, status_code=None, expected_status=ep.expected_status,
|
||||||
|
response_time_ms=None, resolved_ip=None,
|
||||||
|
error=f"dns_error: {e}", timestamp_ns=timestamp_ns,
|
||||||
|
)
|
||||||
|
pin = dns_resolver.pin_resolution(ep.host, resolved_ip)
|
||||||
|
|
||||||
url = f"{ep.scheme}://{ep.host}:{ep.port}{ep.path}"
|
url = f"{ep.scheme}://{dns_resolver.format_host_for_url(ep.host)}:{ep.port}{ep.path}"
|
||||||
try:
|
try:
|
||||||
with dns_resolver.pin_resolution(ep.host, resolved_ip):
|
with pin:
|
||||||
resp = requests.get(
|
resp = requests.get(
|
||||||
url, timeout=timeout, verify=ep.verify_tls, allow_redirects=True
|
url, timeout=timeout, verify=ep.verify_tls, allow_redirects=True
|
||||||
)
|
)
|
||||||
|
|||||||
+17
-4
@@ -5,6 +5,8 @@ from typing import List, Optional
|
|||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
from . import dns_resolver
|
||||||
|
|
||||||
|
|
||||||
class ConfigError(Exception):
|
class ConfigError(Exception):
|
||||||
"""Raised for any problem with the config file's contents."""
|
"""Raised for any problem with the config file's contents."""
|
||||||
@@ -16,9 +18,12 @@ class Endpoint:
|
|||||||
host: str
|
host: str
|
||||||
scheme: str # "http" or "https"
|
scheme: str # "http" or "https"
|
||||||
port: int
|
port: int
|
||||||
dns_server: str # DNS server used to resolve `host` — the system/router
|
|
||||||
# resolver is never consulted, see app/dns_resolver.py
|
|
||||||
expected_status: int
|
expected_status: int
|
||||||
|
# DNS server used to resolve `host` — the system/router resolver is
|
||||||
|
# never consulted, see app/dns_resolver.py. Optional (and unused) when
|
||||||
|
# `host` is already a literal IP address, since there's nothing to
|
||||||
|
# resolve in that case.
|
||||||
|
dns_server: Optional[str] = None
|
||||||
path: str = "/"
|
path: str = "/"
|
||||||
timeout_seconds: Optional[float] = None
|
timeout_seconds: Optional[float] = None
|
||||||
verify_tls: bool = True # set False for endpoints with self-signed certs
|
verify_tls: bool = True # set False for endpoints with self-signed certs
|
||||||
@@ -103,14 +108,22 @@ def load_config(path: str) -> AppConfig:
|
|||||||
f"{ctx} ('{name}'): scheme must be 'http' or 'https', got '{scheme}'"
|
f"{ctx} ('{name}'): scheme must be 'http' or 'https', got '{scheme}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
host = _require(ep, "host", ctx)
|
||||||
|
dns_server = ep.get("dns_server") or None
|
||||||
|
if not dns_resolver.is_ip_literal(host) and not dns_server:
|
||||||
|
raise ConfigError(
|
||||||
|
f"{ctx} ('{name}'): 'dns_server' is required when 'host' is not "
|
||||||
|
"a literal IP address"
|
||||||
|
)
|
||||||
|
|
||||||
timeout = ep.get("timeout_seconds")
|
timeout = ep.get("timeout_seconds")
|
||||||
endpoints.append(
|
endpoints.append(
|
||||||
Endpoint(
|
Endpoint(
|
||||||
name=name,
|
name=name,
|
||||||
host=_require(ep, "host", ctx),
|
host=host,
|
||||||
scheme=scheme,
|
scheme=scheme,
|
||||||
port=int(_require(ep, "port", ctx)),
|
port=int(_require(ep, "port", ctx)),
|
||||||
dns_server=_require(ep, "dns_server", ctx),
|
dns_server=dns_server,
|
||||||
expected_status=int(_require(ep, "expected_status", ctx)),
|
expected_status=int(_require(ep, "expected_status", ctx)),
|
||||||
path=ep.get("path", "/"),
|
path=ep.get("path", "/"),
|
||||||
timeout_seconds=float(timeout) if timeout else None,
|
timeout_seconds=float(timeout) if timeout else None,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ server given for that specific endpoint.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import ipaddress
|
||||||
import socket
|
import socket
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
@@ -20,6 +21,35 @@ import dns.resolver
|
|||||||
_patch_lock = threading.Lock()
|
_patch_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def is_ip_literal(host: str) -> bool:
|
||||||
|
"""Return True if `host` is already a literal IPv4 or IPv6 address.
|
||||||
|
|
||||||
|
Such hosts have nothing to resolve — dnspython does not special-case
|
||||||
|
them, it will happily send a DNS query for e.g. "192.168.0.2" as if it
|
||||||
|
were a hostname and get NXDOMAIN back. Callers should skip resolution
|
||||||
|
entirely for these.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
ipaddress.ip_address(host)
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def format_host_for_url(host: str) -> str:
|
||||||
|
"""Format `host` for embedding directly in a URL.
|
||||||
|
|
||||||
|
IPv6 literals need brackets (e.g. "::1" -> "[::1]"); everything else
|
||||||
|
(hostnames, IPv4 literals) is returned unchanged.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if ipaddress.ip_address(host).version == 6:
|
||||||
|
return f"[{host}]"
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return host
|
||||||
|
|
||||||
|
|
||||||
def resolve(host: str, dns_server: str, timeout: float) -> str:
|
def resolve(host: str, dns_server: str, timeout: float) -> str:
|
||||||
"""Resolve `host` to an IPv4 address using *only* `dns_server`.
|
"""Resolve `host` to an IPv4 address using *only* `dns_server`.
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ endpoints:
|
|||||||
# DNS server used to resolve `host` for THIS endpoint. The
|
# DNS server used to resolve `host` for THIS endpoint. The
|
||||||
# system/router-provided DNS resolver is never used — every lookup
|
# system/router-provided DNS resolver is never used — every lookup
|
||||||
# goes explicitly to this server only.
|
# goes explicitly to this server only.
|
||||||
|
# Required unless `host` is already a literal IP address (see the
|
||||||
|
# entry below), in which case it's optional and ignored.
|
||||||
dns_server: "1.1.1.1"
|
dns_server: "1.1.1.1"
|
||||||
expected_status: 200
|
expected_status: 200
|
||||||
# path: "/" # optional, defaults to "/"
|
# path: "/" # optional, defaults to "/"
|
||||||
@@ -58,4 +60,11 @@ endpoints:
|
|||||||
dns_server: "9.9.9.9"
|
dns_server: "9.9.9.9"
|
||||||
expected_status: 200
|
expected_status: 200
|
||||||
|
|
||||||
|
- name: "example-raw-ip"
|
||||||
|
host: "192.168.0.2" # literal IP -> DNS resolution is skipped entirely
|
||||||
|
scheme: "http"
|
||||||
|
port: 8080
|
||||||
|
# dns_server not needed here
|
||||||
|
expected_status: 200
|
||||||
|
|
||||||
# Add as many endpoints as you like below.
|
# Add as many endpoints as you like below.
|
||||||
|
|||||||
Reference in New Issue
Block a user