add FALLBACK_IP_TLS explanation and bash example
This commit is contained in:
118
docs/FALLBACK_IP_TLS.md
Normal file
118
docs/FALLBACK_IP_TLS.md
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# Fallback IP Routing And TLS Verification
|
||||||
|
|
||||||
|
The app stores the server as a normal URL plus optional fallback IP addresses.
|
||||||
|
The fallback IPs are used as DNS answers for the URL host; the app does not
|
||||||
|
rewrite the request URL to an IP address.
|
||||||
|
|
||||||
|
## Where It Is Implemented
|
||||||
|
|
||||||
|
- `ServerProfile.baseUrl` and `ServerProfile.fallbackIpAddresses` store the URL
|
||||||
|
and fallback list in `app/src/main/java/net/rodakot/ngxhttpmonitoringclient/model/MonitorModels.kt`.
|
||||||
|
- The editor exposes those values as `Base URL` and `Fallback LAN IPs` in
|
||||||
|
`app/src/main/java/net/rodakot/ngxhttpmonitoringclient/ui/MonitorApp.kt`.
|
||||||
|
- `MonitorRepository.buildServer()` normalizes the URL, checks whether plain
|
||||||
|
HTTP is allowed, and parses fallback addresses in
|
||||||
|
`app/src/main/java/net/rodakot/ngxhttpmonitoringclient/data/MonitorRepository.kt`.
|
||||||
|
- `RouteAttemptPlanner.attempts()` tries `SystemDns` first, then `FallbackDns`
|
||||||
|
when fallback addresses exist. When a VPN is active and a LAN network is
|
||||||
|
available, it also adds `LanSystemDns` and `LanFallbackDns`.
|
||||||
|
- `MonitorHttpClient.clientFor()` installs a custom OkHttp `Dns` implementation
|
||||||
|
for fallback routes.
|
||||||
|
|
||||||
|
## Request Flow
|
||||||
|
|
||||||
|
For a server configured like this:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Base URL: https://monitor.example.com
|
||||||
|
Fallback LAN IPs: 192.168.1.20, 10.0.0.5
|
||||||
|
```
|
||||||
|
|
||||||
|
the app builds endpoint URLs from the base URL:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://monitor.example.com/monitor/api
|
||||||
|
https://monitor.example.com/monitor/health
|
||||||
|
https://monitor.example.com/monitor/live
|
||||||
|
```
|
||||||
|
|
||||||
|
The first request uses normal system DNS. If that network request fails before
|
||||||
|
a usable HTTP response is returned, the app tries the fallback route. In that
|
||||||
|
route, OkHttp still receives a request for `https://monitor.example.com/...`,
|
||||||
|
but the custom `FallbackDns` returns `192.168.1.20` and `10.0.0.5` when OkHttp
|
||||||
|
asks how to resolve `monitor.example.com`.
|
||||||
|
|
||||||
|
The LAN variants do the same thing through Android's LAN network socket factory
|
||||||
|
when the active device state includes both VPN and Wi-Fi or Ethernet.
|
||||||
|
|
||||||
|
## Why TLS Still Works
|
||||||
|
|
||||||
|
TLS verification is based on the URL hostname, not the numeric socket address.
|
||||||
|
Because the app keeps the request URL as `https://monitor.example.com/...`,
|
||||||
|
OkHttp still uses `monitor.example.com` for:
|
||||||
|
|
||||||
|
- SNI during the TLS handshake.
|
||||||
|
- Certificate hostname verification after the certificate is received.
|
||||||
|
|
||||||
|
That means the server certificate must be valid for `monitor.example.com`.
|
||||||
|
It does not need to contain `192.168.1.20` as an IP subject alternative name
|
||||||
|
because the client is not asking TLS to verify `192.168.1.20`.
|
||||||
|
|
||||||
|
This is different from requesting:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://192.168.1.20/monitor/api
|
||||||
|
```
|
||||||
|
|
||||||
|
With that URL, the TLS hostname is `192.168.1.20`, so the certificate would
|
||||||
|
need to be valid for the IP address. Adding only an HTTP `Host` header is not
|
||||||
|
enough to fix that because TLS verification happens before the HTTP request is
|
||||||
|
processed.
|
||||||
|
|
||||||
|
If the fallback IP reaches a server that does not present a certificate valid
|
||||||
|
for the original URL host, OkHttp throws an SSL error and the app reports it as
|
||||||
|
`TlsFailure`.
|
||||||
|
|
||||||
|
## Bash Equivalent
|
||||||
|
|
||||||
|
The same behavior in curl is `--resolve`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --resolve monitor.example.com:443:192.168.1.20 \
|
||||||
|
https://monitor.example.com/monitor/api
|
||||||
|
```
|
||||||
|
|
||||||
|
`--resolve` says "connect to this IP for this host and port." The URL still
|
||||||
|
uses `monitor.example.com`, so curl sends SNI for `monitor.example.com` and
|
||||||
|
verifies the certificate against `monitor.example.com`.
|
||||||
|
|
||||||
|
The sample script in this directory wraps that pattern:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash docs/fallback-ip-curl-example.sh \
|
||||||
|
--base-url https://monitor.example.com \
|
||||||
|
--fallback-ip 192.168.1.20 \
|
||||||
|
--fallback-ip 10.0.0.5 \
|
||||||
|
--token '<monitor token>'
|
||||||
|
```
|
||||||
|
|
||||||
|
For Basic Auth:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash docs/fallback-ip-curl-example.sh \
|
||||||
|
--base-url https://monitor.example.com \
|
||||||
|
--fallback-ips "192.168.1.20,10.0.0.5" \
|
||||||
|
--basic-auth 'user:password'
|
||||||
|
```
|
||||||
|
|
||||||
|
For the health endpoint:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash docs/fallback-ip-curl-example.sh \
|
||||||
|
--base-url https://monitor.example.com \
|
||||||
|
--fallback-ip 192.168.1.20 \
|
||||||
|
--endpoint /monitor/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not use `--insecure` for this behavior. `--insecure` disables certificate
|
||||||
|
verification, while the app's fallback keeps verification enabled.
|
||||||
243
docs/fallback-ip-curl-example.sh
Normal file
243
docs/fallback-ip-curl-example.sh
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -u
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'USAGE'
|
||||||
|
Usage:
|
||||||
|
bash docs/fallback-ip-curl-example.sh --base-url URL --fallback-ip IP [options]
|
||||||
|
|
||||||
|
Required:
|
||||||
|
--base-url URL Server root URL, for example https://monitor.example.com
|
||||||
|
--fallback-ip IP Fallback address for the URL host. Can be repeated.
|
||||||
|
--fallback-ips "LIST" Comma or space separated fallback addresses.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--endpoint PATH Monitor endpoint path. Default: /monitor/api
|
||||||
|
--token TOKEN Send TOKEN as X-Monitor-Token.
|
||||||
|
--basic-auth USER:PASS Send HTTP Basic Auth credentials.
|
||||||
|
--connect-timeout SEC Curl connection timeout. Default: 8
|
||||||
|
--max-time SEC Curl total request timeout. Default: 20
|
||||||
|
-h, --help Show this help.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
bash docs/fallback-ip-curl-example.sh \
|
||||||
|
--base-url https://monitor.example.com \
|
||||||
|
--fallback-ip 192.168.1.20 \
|
||||||
|
--fallback-ip 10.0.0.5 \
|
||||||
|
--token secret
|
||||||
|
|
||||||
|
The fallback keeps the URL host as monitor.example.com and only overrides DNS
|
||||||
|
with curl --resolve. For HTTPS, curl still sends SNI for that host and verifies
|
||||||
|
the certificate against that host.
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
die() {
|
||||||
|
printf '%s\n' "$1" >&2
|
||||||
|
printf '\n' >&2
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
add_fallback_ips() {
|
||||||
|
local values ip
|
||||||
|
values="${1//,/ }"
|
||||||
|
for ip in $values; do
|
||||||
|
fallback_list+=("$ip")
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
base_url=""
|
||||||
|
endpoint="/monitor/api"
|
||||||
|
token=""
|
||||||
|
basic_auth=""
|
||||||
|
connect_timeout="8"
|
||||||
|
max_time="20"
|
||||||
|
fallback_list=()
|
||||||
|
|
||||||
|
while (($# > 0)); do
|
||||||
|
case "$1" in
|
||||||
|
--base-url)
|
||||||
|
(($# >= 2)) || die "--base-url requires a value"
|
||||||
|
base_url="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--base-url=*)
|
||||||
|
base_url="${1#*=}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--fallback-ip)
|
||||||
|
(($# >= 2)) || die "--fallback-ip requires a value"
|
||||||
|
add_fallback_ips "$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--fallback-ip=*)
|
||||||
|
add_fallback_ips "${1#*=}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--fallback-ips)
|
||||||
|
(($# >= 2)) || die "--fallback-ips requires a value"
|
||||||
|
add_fallback_ips "$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--fallback-ips=*)
|
||||||
|
add_fallback_ips "${1#*=}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--endpoint)
|
||||||
|
(($# >= 2)) || die "--endpoint requires a value"
|
||||||
|
endpoint="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--endpoint=*)
|
||||||
|
endpoint="${1#*=}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--token)
|
||||||
|
(($# >= 2)) || die "--token requires a value"
|
||||||
|
token="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--token=*)
|
||||||
|
token="${1#*=}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--basic-auth)
|
||||||
|
(($# >= 2)) || die "--basic-auth requires a value"
|
||||||
|
basic_auth="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--basic-auth=*)
|
||||||
|
basic_auth="${1#*=}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--connect-timeout)
|
||||||
|
(($# >= 2)) || die "--connect-timeout requires a value"
|
||||||
|
connect_timeout="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--connect-timeout=*)
|
||||||
|
connect_timeout="${1#*=}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--max-time)
|
||||||
|
(($# >= 2)) || die "--max-time requires a value"
|
||||||
|
max_time="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--max-time=*)
|
||||||
|
max_time="${1#*=}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
die "Unknown argument: $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$base_url" ]] || die "--base-url is required"
|
||||||
|
((${#fallback_list[@]} > 0)) || die "At least one --fallback-ip or --fallback-ips value is required"
|
||||||
|
|
||||||
|
base_url="${base_url%/}"
|
||||||
|
[[ "$endpoint" == /* ]] || endpoint="/$endpoint"
|
||||||
|
url="${base_url}${endpoint}"
|
||||||
|
|
||||||
|
if [[ "$base_url" =~ ^([A-Za-z][A-Za-z0-9+.-]*)://([^/]+) ]]; then
|
||||||
|
scheme="${BASH_REMATCH[1]}"
|
||||||
|
authority="${BASH_REMATCH[2]}"
|
||||||
|
else
|
||||||
|
die "--base-url must be an http or https URL"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$authority" == *"@"* ]]; then
|
||||||
|
die "Put Basic Auth in --basic-auth USER:PASS, not in the URL"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$authority" =~ ^\[([^]]+)\](:([0-9]+))?$ ]]; then
|
||||||
|
host="${BASH_REMATCH[1]}"
|
||||||
|
port="${BASH_REMATCH[3]:-}"
|
||||||
|
elif [[ "$authority" =~ ^([^:]+)(:([0-9]+))?$ ]]; then
|
||||||
|
host="${BASH_REMATCH[1]}"
|
||||||
|
port="${BASH_REMATCH[3]:-}"
|
||||||
|
else
|
||||||
|
die "Could not parse host and port from $authority"
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$scheme" in
|
||||||
|
[Hh][Tt][Tt][Pp][Ss]) port="${port:-443}" ;;
|
||||||
|
[Hh][Tt][Tt][Pp]) port="${port:-80}" ;;
|
||||||
|
*) die "Only http and https URLs are supported" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
curl_args=(
|
||||||
|
--silent
|
||||||
|
--show-error
|
||||||
|
--location
|
||||||
|
--connect-timeout "$connect_timeout"
|
||||||
|
--max-time "$max_time"
|
||||||
|
--header "Accept: application/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
if [[ -n "$token" ]]; then
|
||||||
|
curl_args+=(--header "X-Monitor-Token: $token")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$basic_auth" ]]; then
|
||||||
|
curl_args+=(--user "$basic_auth")
|
||||||
|
fi
|
||||||
|
|
||||||
|
request_once() {
|
||||||
|
local body_file http_code rc
|
||||||
|
|
||||||
|
body_file="$(mktemp)"
|
||||||
|
http_code="$(curl "${curl_args[@]}" "$@" --output "$body_file" --write-out '%{http_code}' "$url")"
|
||||||
|
rc=$?
|
||||||
|
|
||||||
|
if ((rc != 0)); then
|
||||||
|
rm -f "$body_file"
|
||||||
|
return "$rc"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat "$body_file"
|
||||||
|
rm -f "$body_file"
|
||||||
|
|
||||||
|
if [[ "$http_code" == 2* ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\nHTTP %s from monitor endpoint\n' "$http_code" >&2
|
||||||
|
return 22
|
||||||
|
}
|
||||||
|
|
||||||
|
printf 'Trying normal DNS for %s\n' "$host" >&2
|
||||||
|
if request_once; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
rc=$?
|
||||||
|
if ((rc == 22)); then
|
||||||
|
exit "$rc"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'Normal request failed before a usable HTTP response; trying fallback IPs\n' >&2
|
||||||
|
last_rc="$rc"
|
||||||
|
|
||||||
|
for ip in "${fallback_list[@]}"; do
|
||||||
|
[[ -n "$ip" ]] || continue
|
||||||
|
resolve_ip="$ip"
|
||||||
|
if [[ "$resolve_ip" == *:* && "$resolve_ip" != \[*\] ]]; then
|
||||||
|
resolve_ip="[$resolve_ip]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'Trying %s via --resolve %s:%s:%s\n' "$ip" "$host" "$port" "$resolve_ip" >&2
|
||||||
|
if request_once --resolve "${host}:${port}:${resolve_ip}"; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
last_rc=$?
|
||||||
|
done
|
||||||
|
|
||||||
|
exit "$last_rc"
|
||||||
Reference in New Issue
Block a user