85 Commits

Author SHA1 Message Date
8d055682b6 Version bump to 1.5.2 2024-12-14 19:36:59 +01:00
ff8d86ff1d Dependency updates 2024-12-14 19:35:13 +01:00
ce7632d443 Version bump to 1.5.1 2024-10-26 18:23:32 +02:00
cf82db3e87 Update dependencies 2024-10-26 18:19:39 +02:00
fecbe68c7a Cargo update 2024-10-26 18:08:33 +02:00
2e5a2408b4 Add robots meta tag to templates to make it less likely for a crawler to get stuck 2024-08-06 19:36:17 +02:00
3b4e6eba4b Update README, add maintainence mode notice 2024-08-06 19:26:09 +02:00
708fb9c0b3 Improve configurability 2024-08-06 19:17:37 +02:00
0d711648a8 Update idna to 1.0 🥳 2024-08-06 18:40:49 +02:00
1863af50f8 Remove unused configuration option 2024-08-06 18:36:40 +02:00
52d2834e98 Replace lazy_static crate with "new" std::sync::LazyLock 2024-08-06 18:35:00 +02:00
da391003e4 cargo update 2024-08-06 18:12:10 +02:00
7e58423269 Update dependencies 2024-04-21 00:38:30 +02:00
2657aae847 Template fix 2024-04-21 00:26:15 +02:00
13cb85ac5a Added an opt-in to looking up own IP-Address 2024-04-21 00:00:04 +02:00
1a973e09a0 cargo update 2024-04-20 21:47:06 +02:00
f799927f90 Cargo update 2024-03-17 22:02:20 +01:00
8695f0026f lib-humus is now on crates.io 2024-02-11 14:15:13 +01:00
3b552dba8a Downgrade clap to 4.4.18 to support "older" rust versions 2024-02-11 12:38:33 +01:00
1ce60d8291 to_trust_resolver_config() -> to_hickory_resolver_config() 2024-02-11 11:52:33 +01:00
b5097b5a03 cargo update 2024-02-11 11:51:23 +01:00
610842abac Remove unused import 2024-02-11 11:51:08 +01:00
35c71aba64 Use absolute path for icons 2023-12-29 02:51:12 +01:00
d79d949d65 Use the more efficient icon 2023-12-29 02:49:04 +01:00
b3f94b0d90 cargo update 2023-12-29 02:41:33 +01:00
96207f3960 Added a way to display the icon as part of the sitename 2023-12-29 02:37:22 +01:00
cd7a7fbe05 Added a favicon 2023-12-29 02:26:32 +01:00
aaecdb84bb Release 1.2.4 hickory 2023-12-10 18:35:16 +01:00
b08c98376c Update trust_dns to hickory
It doesn't work yet because of:
https://github.com/hickory-dns/hickory-dns/issues/2108
2023-12-10 18:34:20 +01:00
51877fc4c3 1.2.3 quickfix don't encode as numbers (they are nubers internally so that is safe) 2023-12-10 10:43:19 +01:00
396bbdb348 Release 1.2.2 2023-12-10 10:18:31 +01:00
a582c74d18 urlencode queries to external services
and add crt.sh
2023-12-10 10:16:43 +01:00
e8a21ac95f Release 1.2.1 2023-12-09 23:26:12 +01:00
d706e7c614 Update to axum 0.7 2023-12-09 23:21:19 +01:00
0bffa0fd96 Update smaller dependencies 2023-12-09 12:01:00 +01:00
fb0ce1dc0b Update trust_dns to 23.2 2023-12-09 11:54:07 +01:00
a67631fa9b cargo update 2023-12-09 11:17:33 +01:00
636e10f786 Adapted to the new syncronous interface of the HumusEngine 2023-10-30 17:44:33 +01:00
0076db531a cargo update 2023-10-30 17:41:37 +01:00
64e639b0df Release 1.2.0 2023-10-30 01:56:18 +01:00
2f9f01e947 Better template documentation 2023-10-30 01:54:44 +01:00
f2e9e36e99 codename mycelium became lib-humus 2023-10-30 01:54:35 +01:00
912a119361 Made template loading logic reuseable 2023-10-29 20:52:32 +01:00
5adca4fb80 WIP moved templating engine to mycelium 2023-10-29 18:56:44 +01:00
5ac056ef99 Made sure the get_status and update_response callbacks are used correctly 2023-10-29 18:20:57 +01:00
51aa05fe13 Added query settings to the generaliued templating mechanism 2023-10-29 18:10:57 +01:00
de179ea7fa Proper MimeType handling with mycelium 2023-10-29 16:51:43 +01:00
bfa383ddbe Fixed template data piping 2023-10-29 15:36:16 +01:00
a33473fdc9 Moved to a more genral implementation for the response format. 2023-10-29 15:23:47 +01:00
20fb7ee2ff First step to detaching the templating from the logic. 2023-10-29 13:50:22 +01:00
c5a7597561 cargo update 2023-10-29 10:55:40 +01:00
c56cc6edbd Version bump to 1.1.3 2023-10-08 09:27:38 +02:00
5c74de5685 Switch a lot of Strings to Arc<str> 2023-10-08 09:12:06 +02:00
223abdd804 cargo update
This one only removed dependencies 🥳
2023-10-08 08:09:44 +02:00
639d4579e9 Fixed the url pointing to the google dns information^W marketing 2023-08-08 23:15:43 +02:00
4b3a8d5e08 Fixed dns resolver information templates 2023-08-08 23:06:55 +02:00
53da9023da cargo update 2023-08-07 21:59:25 +02:00
4876fb7ea0 Updated README 2023-08-07 21:58:42 +02:00
2aa6baaa57 Removed dns search functionality completely
I don't see it working and not being annoying
2023-08-07 21:52:31 +02:00
daa68bbd5d Reenabled search prevention fqdn dot 2023-08-07 21:42:56 +02:00
231e46a688 Disabled domain search as it caused problems 2023-08-07 21:40:22 +02:00
2fe1b69174 Beeter communication of dns errors 2023-08-07 21:09:14 +02:00
2e1f6a77ac Slightly better handling of invalid domain names 2023-08-07 20:05:25 +02:00
1fe59d24d5 Discard additional responses instead of misattributing them 2023-08-07 19:03:18 +02:00
51d7954d71 Make the sitename render as inline-block to prevent accidental clicking 2023-08-06 13:23:41 +02:00
2fa9de5cf7 Template fix for ip display helper 2023-08-06 04:31:39 +02:00
1013c5365a Small foramtting improvements 2023-08-06 04:03:14 +02:00
cc6bbba3e4 Cargo update 2023-08-06 03:59:09 +02:00
97a3d18e9c Wrote documentation on Configuration 2023-08-06 03:56:53 +02:00
ae95539c7b Multiple small template improvements 2023-08-06 02:08:54 +02:00
cf806ad8f5 Implemented global search config 2023-08-06 01:45:48 +02:00
fef954f6c1 Some small template improvements 2023-08-06 01:32:23 +02:00
e7eba57cb2 Added aliases and some servers to test them 2023-08-06 01:19:02 +02:00
a334eb428a Proper dns configuration! 2023-08-06 00:04:42 +02:00
5c7d880733 Aded ranking of resolvers by a weight value 2023-08-05 23:19:53 +02:00
55897585ff Broke out settings and fixed a bug with the dns resolver not being persisted 2023-08-05 22:53:48 +02:00
d88b15ba02 Persist resover choice using a cookie 2023-08-05 22:36:28 +02:00
fdb23312df Made templates work with new data. 2023-08-05 21:09:56 +02:00
727d9a77cd Template passtrough for dns server information 2023-08-05 18:19:28 +02:00
cc6a025f89 Cargo update 2023-08-04 00:03:24 +02:00
104a072fd6 Configurable multiple dns resolvers 2023-08-04 00:00:21 +02:00
cd8c0455dc First prototype with multiple dns providers 2023-07-23 15:23:44 +02:00
f173eba2ec Cargo update 2023-07-19 22:40:44 +02:00
cd8efdcb44 Remove unneccessary tokio features. 2023-07-19 22:39:13 +02:00
455ee751c4 cargo update 2023-06-18 20:32:15 +02:00
32 changed files with 2683 additions and 1261 deletions

2195
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,26 +1,30 @@
[package]
name = "echoip-slatecave"
version = "0.1.0"
version = "1.5.2"
edition = "2021"
authors = ["Slatian <baschdel@disroot.org>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
axum = { version = "0.6", features = ["macros", "headers"] }
axum-client-ip = "0.4"
clap = { version = "4", features = ["derive"] }
governor = "0.5"
idna = "0.3"
lazy_static = "1.4.0"
lib-humus = { version="0.2", features=["axum-view+cookie"] }
axum = { version = "0.7", features = ["macros"] }
axum-extra = { version = "0.9", features = ["cookie", "typed-header"] }
axum-client-ip = "0.6"
clap = { version = "4.5", features = ["derive"] }
governor = "0.8"
idna = "1.0"
parking_lot = "0.12"
regex = "1.7"
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
regex = "1.11"
serde = { version = "1", features = ["derive","rc"] }
tokio = { version = "1", features = ["macros","signal"] }
tera = "1"
toml = "0.7"
tower = "0.4"
tower-http = { version = "0.4", features = ["fs"] }
trust-dns-proto = "0.22"
trust-dns-resolver = "0.22"
maxminddb = "0.23"
toml = "0.8"
tower = "0.5"
tower-http = { version = "0.6", features = ["fs"] }
hickory-proto = "0.24"
hickory-resolver = { version = "0.24", features = ["dns-over-rustls","dns-over-https","dns-over-quic","native-certs"] }
maxminddb = "0.24"
mime = "0.3"
http = "1.2"

209
Configuration.md Normal file
View File

@ -0,0 +1,209 @@
# Configuring echoip-slatecave
The configuration of echoip-slatecave consists of one [toml](https://toml.io) file for configuring the service itself and one for configuring the template. This document covers on the service configuration.
By default the service tried to load `echoip.toml` from the current pwd as its configurtion, a custom path can be specified using the `-c <configfile>` option.
The configuration file consists of multiple sections of options grouped by feature.
* [server](#server) - Contains global options for the service
* [dns](#dns)
* [geoip](#geoip)
* [template](#template) - templating and rendering
* [ratelimit](#ratelimit)
The default configuration tries to be working out of the box while providing as little security footguns as possible. Have a look at the `server` and `dns` sections that is where most security related options are.
## [server]
### `listen_on`
Configures the bind address and port the service will listen on.
It uses the format `<ip-address>:<port>`, for the ip-address use `127.0.0.1` for only allowing local connections or `0.0.0.0` for allowing IPv4 connections from anywhere, `[::]` for allowing IPv6 connections from anywhere. For the port pick a free TCP port on your machine, default is `3000`.
This option can be overridden by the `-l <ip-address>:<port>` option on the commandline.
### `ip_header`
Configures which http header that contains the real client IP-Address or tells the service to use the IP-Address used to connect to it when in use without a proxy server.
Possible values are best covered by the [documentation for the underlying datatype](https://docs.rs/axum-client-ip/latest/axum_client_ip/enum.SecureClientIpSource.html).
When using a reverse Proxy `RightmostXForwardedFor` is usually what you want.
When using without a reverse Proxy set to `ConnectInfo`.
Please keep in mind that the ratelimit depends on the IP-Address being non-spoofable which is only given if the setting here matches the one of your proxy.
### `allow_private_ip_lookup`
Defaults to `false`, set to `true` to allow looking up IP-Addresses that fall into the private IP-Range. Enabling is not recommended when the server is publically accessible.
### `static_location`
When specified allows overriding the location where echoip-slatecave serve static files from (the default is the `static` directory under the [template_location](#template_location) )
## [dns]
### `allow_forward_lookup`
When set to `true`, allows resolving Domain names over the webinterface for every configured dns resolver.
### `allow_reverse_lookup`
When set to `true`, allows looking up domain names for IP-Addresses using reverse dns lookups for every configured resolver.
### `hidden_suffixes`
Configure it with a list of suffixes of domin names you don't want to leak out to the web interface.
If an entry matches, echoip-slatecave will try its best to pretend that resolving the name resulted in it not existing.
Example:
```toml
[dns]
hidden_suffixes = [".local",".box",".internal"]
```
This configuration option will not be exposed over the webinterface.
### System Resolver
By default echoip-slatecave uses the system configuration for dns like most other programs.
In case this is undesired one can disable it by setting `enable_system_resolver` to false.
```toml
[dns]
enable_system_resolver = false
```
In case you want to use the system resolver and customize it.
`system_resolver_name`
: Equivalent to the `display_name` of a custom resolver, default: "System"
`system_resolver_id`
: Equivalent to the `key` of a custom resolver, default "system"
`system_resolver_weight`
: Equivalent to the `weight` of a custom resolver, default: 1000
### Custom resolvers
It is possible to confgure custom resolvers in plce of or in addition to the default system resolver.
To do this create a new section in the configuration file called `[dns.resolver.<key>]` where `<key>` is an url-friendly name for the resolver.
In this section one can fonfigure functional and cosmetic aspects of the resolver.
`display_name`
: The name that will be used for this resolver in the UI, it should be short and descriptive. Naming a main feature helps with keeping track of which Server is which when you have multiple servers with similar names. (required)
`info_url`
: A url pinting to the page containing service information and a technical overview of the dns server. (optional)
`aliases`
: A list of short strings that can be used to quickly typing in the desired server, inteded for power users.
`weight`
: An integer that helps with ranking multiple resolvers, oneswith higher weights will be displayed further up the lists of available options, the one with the highest weight will become the default resolver.
`servers`
: A list of socket addresses, that is `<ip-address>:<port>` pointing to the available dns servers. Leaving the port out is not yet supported.
`protocol`
: Which protocol to use for accessing the dns server. While `udp` works for almost all servers using `tls` is recommended when available.
`tls_dns_name`
: When using `tls`, `https` or `quic` this name helps the server knowing want ([SNI](https://en.wikipedia.org/wiki/Server_Name_Indication)). Usually this is the domain name of the dns server.
Available protocols:
| Protocol | Default Port |
|----------|--------------|
| udp | 53 |
| tcp | 53 |
| tls | 853 |
| https | 443 |
| quic | 443 |
Example configuration:
```toml
[dns.resolver.digitalcourage]
display_name = "Digitalcourage"
info_url = "https://digitalcourage.de/support/zensurfreier-dns-server"
aliases = ["dc","dc3","digitalcourage3"]
weight = 990
servers = ["5.9.164.112:853","[2a01:4f8:251:554::2]:853"]
protocol = "tls"
tls_dns_name = "dns3.digitalcourage.de"
```
## [geoip]
These options configure paths to maxmind (or compatible) databses. The Official databases are available after signing up on [maxmind.com](https://maxmind.com). (In case someone knows a similar source of IP to geolocation mapping under a less propritetary license please contact me.)
To get the full functionalityyou need the ASN and City databases in mmdb format.
The `asn_database` and `location_database` fields are for their filepaths.
Example:
```toml
[geoip]
asn_database = "mmdb/GeoLite2-ASN.mmdb"
location_database = "mmdb/GeoLite2-City.mmdb"
```
Note: When echoip-slatecave rececieves a `SIGUSR1` posix signal it will attempt to reload the mmdb files. This is useful for keeping the databses up to date without having to restart the service.
## [template]
### `template_location`
This option contains the path to the direcotry containing the templates. It is overridden by the `-t <template-location>` command line option.
It can contain a glob pattern, bit make sure to configure the [`static_location`](#static_location) if it does.
### `extra_config`
This points to the toml file containing the configuration for the template itself, its content depends on what the template expects. This option is overridden by the `-e <extra-config>` command line option.
### `text_user_agents`
A list of Prefixes of UserAgents that should be served plain text by default.
Example:
```toml
[template]
# Give curl the plain text version by default
text_user_agents = ["curl/"]
```
## [ratelimit]
Configure a Quota for the Rate limiter.
Note: The ratelimiter depends on the [ip_header](#ip_header) setting to be coorect, otherwise spoofing is possible enabling Denail of Service type attacks.
`per_minute`
: Integer of how many requests are allowed (and regnerate) per minute.
`burst`
: Integer of how many requests are additionally allowed.
Note: The ratelimit is implemented using the [governor](https://docs.rs/governor/0.6.0/governor/) crate.
Example:
```toml
[ratelimit]
per_minute = 20
burst = 15
```
This allows up to 20+15=35 rquests without running into any limit. For every 3 seconds passing one additional request is granted (up to the limit of 35), which amounts to 60/3 = 20 requests per minute.

View File

@ -1,16 +1,25 @@
# echoip-slatecave
This is a service inspired by ifconfig.co, but built from scratch with a more useful feature set (in rust !!1!).
This is a service inspired by ifconfig.co, but built from scratch with a more useful feature set. Currently live on [echoip.slatecave.net](https://echoip.slatecave.net).
It is Licensed under the AGPL-v3 license.
## Maintainence Mode
This project is in maintanance mode.
This means the following will happen:
* Dependency updates
* Bugfixes
* Small quality of life improvements
But no active feature development by Slatian.
## Building
Simply run `cargo build` after cloning. The binary should be called `target/debug/echoip-slatecave`.
To make a release build (the one you want to have on your server) run `cargo build --relese`, the binary will end up in `target/release/echoip-slatecave`.
NOTE: As of 2023-02-18 You need at least version 1.65 of the rust compiler. Consider using rustup.
To make a release build (the one you want to have on your server) run `cargo build --release`, the binary will end up in `target/release/echoip-slatecave`.
## Usage and configuration
@ -36,18 +45,45 @@ A less sane, but better for testing version can be found in [echoip_test.toml](e
Templates in the templates folder exist for every rich page that `echoip-slatecave` supports.
The code that rendeers them can be found in [src/templating_engine.rs](src/templating_engine.rs).
There is a configuration file for templates which by default is the `extra.toml` file in the template directory. Its content is exposed to the templates in the `extra` struct.
The default templates should make use of everything exposed to the templating part, the `data.result` or `data` object is usually what you get when you ask for the json version.
In addition to that the following fields are accessible from inside the template:
`view`
: The views name (the basename of the template file, i.e. `404` or `ip`)
`format`
: The format name (`html`, `text`, `json`)
`mimetype`
: The resulting mimetype (i.e. `text/plain; charset=utf-8`)
`http_status`
: The numeric HTTP Status-Code at the time of rendering the template.
`language`
: The language requested by the browser.
`dns_resolvers`
: A list of [Selectable](src/settings.rs) structs representing the available DNS-Resolvers.
`dns_resolver_id`
: The id of the currently selected DNS-Resolver
The templates are covered by the AGPL as well, please share them with your users if you modified them.
### GeoLite2 database
### Geolocation databases
For geolocation to work you need a MaxMind format database, for full functionality you need the GeoLite2-ASN and GeoLite2-City databses. Unfortunately you have to sign up with [MaxMind](https://maxmind.com) to obtain them. Once you have a license key there is a helper script in [contrib/maxmind-download.sh](contrib/maxmind-download.sh) that helps you with keeping the databse updated.
As an alternative to MaxMind there is also [DB-IP who offer their free databases without a login](https://db-ip.com/db/lite.php). You want the City and ASN databases in mmdb format.
**Don't forget to set the atttribution in the template configuration appropriately.**
See the file `templates/extra.toml`.
Since v1.0 echoip-slatecave reloads the databses when it rececieves a `USR1` signal.
## Security Considerations
@ -67,6 +103,12 @@ For a public service you should use a reverse proxy like Caddy, apache2 or nginx
`echoip-slatecave` has some simle ratelimiting built in (see the `[ratelimit]` section in the configuration file) this should help you with too frequest automated requests causung high load.
The default configuration is pretty liberal so that the average human probably won't notice the rate limit, but a misbehavingig bot will be limited to one request every 3 seconds after 15 requests.
## TODO
* [ ] Add a way to configure just the dns server addresses and derive the port from the protocol.
* [ ] Add an about page for the system resolver
* [ ] Expose DNS responses from the additional on the web interface
## License
Copyright (c) 2023 Slatian

View File

@ -5,7 +5,7 @@
listen_on = "127.0.0.1:3000"
# What header your reverse proxy sets that contains the real ip-address
# Possible Values: Every Vaiation of SecureClientIpSource in the axum_client_ip package
# Possible Values: Every Variation of SecureClientIpSource in the axum_client_ip package
# https://docs.rs/axum-client-ip/latest/axum_client_ip/enum.SecureClientIpSource.html
#ip_header = "RightmostXForwardedFor"
# When you don't want to use a proxy server:
@ -35,7 +35,7 @@ hidden_suffixes = [".com"]
asn_database = "mmdb/GeoLite2-ASN.mmdb"
location_database = "mmdb/GeoLite2-City.mmdb"
# If anyone knows a free (as in freedom) groip database
# If anyone knows a free (as in freedom) geoip database
# please open an issue so I can integrate it
# https://codeberg.org/slatian/service.echoip-slatecave
@ -61,3 +61,61 @@ burst = 15
#Note: The ratelimit is implemented using the governor crate
[dns.resolver.digitalcourage]
display_name = "Digitalcourage"
info_url = "https://digitalcourage.de/support/zensurfreier-dns-server"
aliases = ["dc","dc3","digitalcourage3"]
weight = 990
servers = ["5.9.164.112:853","[2a01:4f8:251:554::2]:853"]
protocol = "tls"
tls_dns_name = "dns3.digitalcourage.de"
[dns.resolver.quad9]
display_name = "Quad9"
info_url = "https://www.quad9.net/service/service-addresses-and-features/"
aliases = ["q9","9999"]
weight = 980
servers = ["9.9.9.9:853", "149.112.112.112:853", "[2620:fe::fe]:853", "[2620:fe::9]:853"]
protocol = "tls"
tls_dns_name = "dns.quad9.net"
[dns.resolver.quad9_ecs]
display_name = "Quad9 with ecs"
info_url = "https://www.quad9.net/service/service-addresses-and-features/"
aliases = ["q9ecs","9999ecs","ecs"]
weight = 970
servers = ["9.9.9.11:853", "149.112.112.11:853", "[2620:fe::fe:11]:853", "[2620:fe::11]:853"]
protocol = "tls"
tls_dns_name = "dns11.quad9.net"
[dns.resolver.quad9_unvalidated]
display_name = "Quad9 unvalidated"
info_url = "https://www.quad9.net/service/service-addresses-and-features/"
aliases = ["q9u","9999u"]
weight = 960
servers = ["9.9.9.10:853", "149.112.112.10:853", "[2620:fe::fe:10]:853", "[2620:fe::10]:853"]
protocol = "tls"
tls_dns_name = "dns10.quad9.net"
[dns.resolver.cloudflare]
display_name = "Cloudflare"
info_url = "https://www.cloudflare.com/dns/"
aliases = ["cf","1111"]
weight = 450
servers = ["1.1.1.1:853", "1.0.0.1:853", "[2606:4700:4700::1111]:853", "[2606:4700:4700::1001]:853"]
protocol = "tls"
tls_dns_name = "cloudflare-dns.com"
[dns.resolver.google]
display_name = "Google"
info_url = "https://developers.google.com/speed/public-dns/docs/using"
aliases = ["goo","8888"]
weight = 440
servers = ["8.8.8.8:53", "8.4.4.8:53", "[2001:4860:4860::8888]:53", "[2001:4860:4860::8844]:53"]
protocol = "udp"

110
src/config/dns.rs Normal file
View File

@ -0,0 +1,110 @@
use serde::{Deserialize,Serialize};
use hickory_resolver::config::Protocol;
use hickory_resolver::config::ResolverConfig as HickoryResolverConfig;
use hickory_resolver::config::NameServerConfig;
use std::sync::Arc;
use std::collections::HashMap;
use std::net::SocketAddr;
#[derive(Deserialize, Clone)]
#[serde(default)]
pub struct DnsConfig {
pub allow_forward_lookup: bool,
pub allow_reverse_lookup: bool,
pub hidden_suffixes: Vec<String>,
pub resolver: HashMap<Arc<str>,DnsResolverConfig>,
pub enable_system_resolver: bool,
pub system_resolver_name: Arc<str>,
pub system_resolver_weight: i32,
pub system_resolver_id: Arc<str>,
}
#[derive(Deserialize, Serialize, Clone)]
#[serde(rename_all="lowercase")]
pub enum DnsProtocol {
Udp,
Tcp,
Tls,
Https,
Quic,
}
#[derive(Deserialize, Serialize, Clone)]
pub struct DnsResolverConfig {
pub display_name: Arc<str>,
#[serde(default)]
pub info_url: Option<Arc<str>>,
#[serde(default)]
pub aliases: Vec<Arc<str>>,
#[serde(default="zero")]
pub weight: i32,
pub servers: Vec<SocketAddr>,
pub protocol: DnsProtocol,
pub tls_dns_name: Option<Arc<str>>,
#[serde(skip_serializing)] //Don't leak our bind address to the outside
pub bind_address: Option<SocketAddr>,
#[serde(default="default_true", alias="trust_nx_responses")]
pub trust_negative_responses: bool,
}
fn zero() -> i32 {
return 0;
}
fn default_true() -> bool {
return true;
}
impl Default for DnsConfig {
fn default() -> Self {
DnsConfig {
allow_forward_lookup: true,
allow_reverse_lookup: false,
hidden_suffixes: Vec::new(),
resolver: Default::default(),
enable_system_resolver: true,
system_resolver_name: "System".into(),
system_resolver_weight: 1000,
system_resolver_id: "system".into(),
}
}
}
impl Into<Protocol> for DnsProtocol {
fn into(self) -> Protocol {
match self {
Self::Udp => Protocol::Udp,
Self::Tcp => Protocol::Tcp,
Self::Tls => Protocol::Tls,
Self::Https => Protocol::Https,
Self::Quic => Protocol::Quic,
}
}
}
impl DnsResolverConfig {
pub fn to_hickory_resolver_config(
&self
) -> HickoryResolverConfig {
let mut resolver = HickoryResolverConfig::new();
for server in &self.servers {
resolver.add_name_server(NameServerConfig{
socket_addr: *server,
protocol: self.protocol.clone().into(),
tls_dns_name: self.tls_dns_name.clone().map(|s| s.to_string()),
trust_negative_responses: self.trust_negative_responses,
tls_config: None,
bind_addr: self.bind_address,
});
}
// Not configuring domain search here because searching
// on the resolver level is a bad idea unless we are
// taling about the system resolver which we
// can't tell what to do (which is good!)
return resolver;
}
}

View File

@ -1,8 +1,14 @@
use axum_client_ip::SecureClientIpSource;
use serde::Deserialize;
use std::net::SocketAddr;
use std::num::NonZeroU32;
#[derive(serde::Deserialize, Default, Clone)]
mod dns;
pub use crate::config::dns::{DnsConfig, DnsResolverConfig};
#[derive(Deserialize, Default, Clone)]
pub struct EchoIpServiceConfig {
pub server: ServerConfig,
pub dns: DnsConfig,
@ -11,59 +17,41 @@ pub struct EchoIpServiceConfig {
pub ratelimit: RatelimitConfig,
}
#[derive(serde::Deserialize, Clone)]
#[derive(Deserialize, Clone)]
pub struct ServerConfig {
pub listen_on: SocketAddr,
pub ip_header: SecureClientIpSource,
pub allow_private_ip_lookup: bool,
pub static_location: Option<String>,
}
#[derive(serde::Deserialize, Clone)]
pub struct DnsConfig {
pub allow_forward_lookup: bool,
pub allow_reverse_lookup: bool,
pub hidden_suffixes: Vec<String>,
//Future Idea: allow custom resolver
}
#[derive(serde::Deserialize, Clone)]
#[derive(Deserialize, Clone)]
pub struct GeoIpConfig {
pub asn_database: Option<String>,
pub location_database: Option<String>,
}
#[derive(serde::Deserialize, Clone)]
#[derive(Deserialize, Clone)]
pub struct TemplateConfig {
pub template_location: String,
pub extra_config: Option<String>,
pub text_user_agents: Vec<String>,
}
#[derive(serde::Deserialize, Clone)]
#[derive(Deserialize, Clone)]
pub struct RatelimitConfig {
pub per_minute: NonZeroU32,
pub burst: NonZeroU32,
}
impl Default for ServerConfig {
fn default() -> Self {
ServerConfig {
listen_on: "127.0.0.1:3000".parse().unwrap(),
ip_header: SecureClientIpSource::ConnectInfo,
allow_private_ip_lookup: false,
static_location: None,
}
}
}
impl Default for DnsConfig {
fn default() -> Self {
DnsConfig {
allow_forward_lookup: true,
allow_reverse_lookup: false,
hidden_suffixes: Vec::new(),
}
}
}
@ -95,3 +83,4 @@ impl Default for RatelimitConfig {
}
}
}

View File

@ -1,31 +1,28 @@
use axum::{
body::Body,
extract::{
self,
Query,
State,
Extension,
},
headers,
http::Request,
handler::Handler,
http::Request,
middleware::{self, Next},
response::Response,
Router,
routing::get,
TypedHeader,
};
use axum_client_ip::SecureClientIp;
use axum_extra::headers;
use axum_extra::TypedHeader;
use clap::Parser;
use lazy_static::lazy_static;
use regex::Regex;
use tera::Tera;
use serde::{Deserialize,Serialize};
use tower::ServiceBuilder;
use tower_http::services::ServeDir;
use trust_dns_resolver::{
TokioAsyncResolver,
// config::ResolverOpts,
// config::ResolverConfig,
};
use hickory_resolver::Name;
use hickory_resolver::TokioAsyncResolver;
use tokio::signal::unix::{
signal,
@ -33,17 +30,24 @@ use tokio::signal::unix::{
};
use tokio::task;
use std::fs;
use std::collections::HashMap;
use std::net::IpAddr;
use std::net::SocketAddr;
use std::sync::Arc;
use std::sync::LazyLock;
use lib_humus::TemplateEngineLoader;
use lib_humus::read_toml_from_file;
use lib_humus::HumusEngine;
mod config;
mod geoip;
mod idna;
mod ipinfo;
mod ratelimit;
mod settings;
mod simple_dns;
mod templating_engine;
mod idna;
mod view;
use crate::geoip::{
QueryAsn,
@ -52,54 +56,64 @@ use crate::geoip::{
LocationResult,
};
use crate::idna::IdnaName;
use crate::templating_engine::{
View,
ResponseFormat,
TemplateSettings,
};
use crate::simple_dns::DnsLookupResult;
use crate::settings::*;
use crate::view::View;
use crate::ipinfo::{AddressCast,AddressInfo,AddressScope};
#[derive(serde::Deserialize, serde::Serialize, Clone)]
type TemplatingEngine = HumusEngine<View,QuerySettings,ResponseFormat>;
#[derive(Deserialize, Serialize, Clone)]
pub struct SettingsQuery {
format: Option<ResponseFormat>,
lang: Option<String>,
dns: Option<String>,
dns_self_lookup: Option<bool>,
}
#[derive(serde::Deserialize, serde::Serialize, Clone)]
#[derive(Deserialize, Serialize, Clone)]
pub struct SearchQuery {
query: Option<String>,
}
#[derive(serde::Deserialize, serde::Serialize, Clone)]
#[derive(Serialize, Clone)]
pub struct IpResult {
address: IpAddr,
hostname: Option<String>,
asn: Option<AsnResult>,
location: Option<LocationResult>,
ip_info: AddressInfo,
used_dns_resolver: Option<Arc<str>>,
reverse_dns_disabled_for_privacy: bool,
}
// We need this one to hide the partial lookup field when irelevant
pub fn not(b: &bool) -> bool { !b }
#[derive(serde::Deserialize, serde::Serialize, Default, Clone)]
#[derive(Serialize, Clone)]
pub struct DigResult {
records: simple_dns::DnsLookupResult,
#[serde(skip_serializing_if = "IdnaName::was_ascii")]
idn: IdnaName,
#[serde(skip_serializing_if = "not")]
partial_lookup: bool,
used_dns_resolver: Arc<str>,
}
struct ServiceSharedState {
templating_engine: templating_engine::Engine,
dns_resolver: TokioAsyncResolver,
asn_db: geoip::MMDBCarrier,
location_db: geoip::MMDBCarrier,
config: config::EchoIpServiceConfig,
templating_engine: TemplatingEngine,
dns_resolvers: HashMap<Arc<str>,TokioAsyncResolver>,
dns_resolver_aliases: HashMap<Arc<str>,Arc<str>>,
asn_db: geoip::MMDBCarrier,
location_db: geoip::MMDBCarrier,
config: config::EchoIpServiceConfig,
}
// Stores configuration that is derived from the original configuration
#[derive(Clone)]
struct DerivedConfiguration {
dns_resolver_selectables: Vec<Selectable>,
default_resolver: Arc<str>,
}
#[derive(Parser)]
@ -108,7 +122,7 @@ struct CliArgs {
#[arg(short, long)]
config: Option<String>,
#[arg(short, long)]
listen_on: Option<String>,
listen_on: Option<SocketAddr>,
#[arg(short, long)]
template_location: Option<String>,
#[arg(short,long)]
@ -127,23 +141,6 @@ fn match_domain_hidden_list(domain: &String, hidden_list: &Vec<String>) -> bool
return false;
}
fn read_toml_from_file<T: for<'de> serde::Deserialize<'de>>(path: &String) -> Option<T> {
let text = match fs::read_to_string(path) {
Ok(t) => t,
Err(e) => {
println!("Error while reading file '{path}': {e}");
return None;
}
};
match toml::from_str(&text) {
Ok(t) => Some(t),
Err(e) => {
println!("Unable to parse file '{path}':\n{e}");
return None;
}
}
}
#[tokio::main]
async fn main() {
// Parse Command line arguments
@ -153,9 +150,11 @@ async fn main() {
let config: config::EchoIpServiceConfig = match cli_args.config {
Some(config_path) => {
match read_toml_from_file::<config::EchoIpServiceConfig>(&config_path) {
Some(c) => c,
None => {
println!("Could not read confuration file, exiting.");
Ok(c) => c,
Err(e) => {
println!("Could not read confuration file!");
println!("{e}");
println!("Exiting ...");
::std::process::exit(1);
}
}
@ -164,47 +163,25 @@ async fn main() {
};
// Initalize Tera templates
let mut template_base_dir = match cli_args.template_location {
Some(template_base_dir) => template_base_dir,
None => (&config.template.template_location).to_owned(),
};
if !template_base_dir.ends_with("/") {
template_base_dir = template_base_dir + "/";
}
let template_extra_config = match &cli_args.extra_config {
Some(path) => read_toml_from_file(path),
None => match &config.template.extra_config {
Some(path) => read_toml_from_file(path),
None => {
println!("Trying to read default template configuration ...");
println!("(If this fails that may be ok, depending on your template)");
read_toml_from_file(&(template_base_dir.clone()+"extra.toml"))
},
},
};
let template_glob = template_base_dir.clone()+"*";
println!("Parsing Templates from '{}' ...", &template_glob);
let res = Tera::new((template_glob).as_str());
let tera = match res {
Ok(t) => t,
let template_loader = TemplateEngineLoader::new(
config.template.template_location.clone(),
config.template.extra_config.clone()
)
.cli_template_location(cli_args.template_location)
.cli_extra_config_location(cli_args.extra_config);
let templating_engine = match template_loader.load_templates() {
Ok(t) => t.into(),
Err(e) => {
println!("Template parsing error(s): {}", e);
println!("{e}");
::std::process::exit(1);
}
};
let templating_engine = templating_engine::Engine{
tera: tera,
template_config: template_extra_config,
};
// Static file directory
let static_file_directory = cli_args.static_location.unwrap_or(
config.server.static_location.clone().unwrap_or(
template_base_dir+"/static"
)
);
let static_file_directory = template_loader.base_dir()+"/static";
println!("Static files will be served from: {static_file_directory}");
@ -225,32 +202,71 @@ async fn main() {
location_db.reload_database().ok();
// Initalize DNS resolver with os defaults
println!("Initalizing dns resolver ...");
println!("Initalizing dns resolvers ...");
println!("Using System configuration ...");
let res = TokioAsyncResolver::tokio_from_system_conf();
//let res = TokioAsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default());
let dns_resolver = match res {
Ok(resolver) => resolver,
Err(e) => {
println!("Error while setting up dns resolver: {e}");
::std::process::exit(1);
let mut dns_resolver_selectables = Vec::<Selectable>::new();
let mut dns_resolver_map: HashMap<Arc<str>,TokioAsyncResolver> = HashMap::new();
let mut dns_resolver_aliases: HashMap<Arc<str>,Arc<str>> = HashMap::new();
if config.dns.enable_system_resolver {
println!("Initalizing System resolver ...");
let res = TokioAsyncResolver::tokio_from_system_conf();
let resolver = match res {
Ok(resolver) => resolver,
Err(e) => {
println!("Error while setting up dns resolver: {e}");
::std::process::exit(1);
}
};
dns_resolver_map.insert(config.dns.system_resolver_id.clone(), resolver);
dns_resolver_selectables.push(Selectable {
id: config.dns.system_resolver_id.clone(),
name: config.dns.system_resolver_name.clone(),
weight: config.dns.system_resolver_weight,
});
}
for (key, resolver_config) in &config.dns.resolver {
println!("Initalizing {} resolver ...", key);
let resolver = TokioAsyncResolver::tokio(
resolver_config.to_hickory_resolver_config(),
Default::default()
);
dns_resolver_map.insert(key.clone(), resolver);
dns_resolver_selectables.push(Selectable {
id: key.clone(),
name: resolver_config.display_name.clone(),
weight: resolver_config.weight,
});
for alias in &resolver_config.aliases {
dns_resolver_aliases.insert(alias.clone(),key.clone());
}
};
let listen_on = config.server.listen_on;
}
let listen_on = cli_args.listen_on.unwrap_or(config.server.listen_on);
let ip_header = config.server.ip_header.clone();
// Initialize shared state
let shared_state = Arc::new(
ServiceSharedState {
templating_engine: templating_engine,
dns_resolver: dns_resolver,
dns_resolvers: dns_resolver_map,
dns_resolver_aliases: dns_resolver_aliases,
asn_db: asn_db,
location_db: location_db,
config: config.clone(),
});
dns_resolver_selectables.sort_by(|a,b| b.weight.cmp(&a.weight));
let default_resolver = dns_resolver_selectables.get(0)
.map(|s| s.id.clone() )
.unwrap_or("none".into());
let derived_config = DerivedConfiguration {
dns_resolver_selectables: dns_resolver_selectables,
default_resolver: default_resolver,
};
let signal_usr1_handlers_state = shared_state.clone();
task::spawn(async move {
@ -276,6 +292,8 @@ async fn main() {
.route("/", get(handle_default_route))
.route("/dig/:name", get(handle_dig_route_with_path))
.route("/ip/:address", get(handle_ip_route_with_path))
.route("/dns_resolver/:resolver", get(handle_dns_resolver_route_with_path))
.route("/dns_resolver", get(handle_dns_resolver_route))
.route("/ua", get(user_agent_handler))
.route("/hi", get(hello_world_handler))
.fallback_service(
@ -290,72 +308,88 @@ async fn main() {
config.ratelimit.per_minute, config.ratelimit.burst))
.layer(middleware::from_fn(ratelimit::rate_limit_middleware))
.layer(Extension(config))
.layer(middleware::from_fn(format_and_language_middleware))
.layer(Extension(derived_config))
.layer(middleware::from_fn(settings_query_middleware))
)
;
println!("Starting Server ...");
println!("Starting Server on {} ...",listen_on);
axum::Server::bind(&listen_on)
.serve(app.into_make_service_with_connect_info::<std::net::SocketAddr>())
let listener = tokio::net::TcpListener::bind(&listen_on).await.unwrap();
axum::serve(listener, app.into_make_service_with_connect_info::<std::net::SocketAddr>())
.await
.unwrap();
}
async fn format_and_language_middleware<B>(
async fn settings_query_middleware(
Query(query): Query<SettingsQuery>,
Extension(config): Extension<config::EchoIpServiceConfig>,
Extension(derived_config): Extension<DerivedConfiguration>,
cookie_header: Option<TypedHeader<headers::Cookie>>,
user_agent_header: Option<TypedHeader<headers::UserAgent>>,
mut req: Request<B>,
next: Next<B>
mut req: Request<Body>,
next: Next
) -> Response {
let mut format = query.format;
let mut dns_resolver_id = derived_config.default_resolver;
if let Some(resolver_id) = query.dns {
dns_resolver_id = resolver_id.into();
} else if let Some(cookie_header) = cookie_header {
if let Some(resolver_id) = cookie_header.0.get("dns_resolver") {
dns_resolver_id = resolver_id.into();
}
}
// Try to guess type from user agent
if format.is_none() {
if let Some(TypedHeader(user_agent)) = user_agent_header {
let ua = user_agent.as_str();
for tua in config.template.text_user_agents {
if ua.starts_with(&tua) {
format = Some(ResponseFormat::TextPlain);
format = Some(ResponseFormat::Text);
break;
}
}
}
}
// Add the request settings extension
req.extensions_mut().insert(TemplateSettings{
format: format.unwrap_or(ResponseFormat::TextHtml),
req.extensions_mut().insert(QuerySettings{
format: format.unwrap_or(ResponseFormat::Html),
lang: query.lang.unwrap_or("en".to_string()),
available_dns_resolvers: derived_config.dns_resolver_selectables,
dns_resolver_id: dns_resolver_id,
dns_disable_self_lookup: !query.dns_self_lookup.unwrap_or(false),
});
next.run(req).await
}
async fn not_found_handler(
State(arc_state): State<Arc<ServiceSharedState>>,
Extension(settings): Extension<TemplateSettings>,
Extension(settings): Extension<QuerySettings>,
) -> Response {
let state = Arc::clone(&arc_state);
state.templating_engine.render_view(
&settings,
&View::NotFound,
).await
View::NotFound,
)
}
async fn hello_world_handler(
State(arc_state): State<Arc<ServiceSharedState>>,
Extension(settings): Extension<TemplateSettings>,
Extension(settings): Extension<QuerySettings>,
) -> Response {
let state = Arc::clone(&arc_state);
state.templating_engine.render_view(
&settings,
&View::Message{
View::Message{
title: "Hey There!".to_string(),
message: "You,You are an awesome Creature!".to_string()
message: "You are an awesome Creature!".to_string()
},
).await
)
}
@ -368,20 +402,33 @@ async fn user_agent_handler(
async fn handle_default_route(
Query(search_query): Query<SearchQuery>,
State(arc_state): State<Arc<ServiceSharedState>>,
Extension(settings): Extension<TemplateSettings>,
Extension(settings): Extension<QuerySettings>,
user_agent_header: Option<TypedHeader<headers::UserAgent>>,
SecureClientIp(address): SecureClientIp
SecureClientIp(client_ip): SecureClientIp
) -> Response {
let state = Arc::clone(&arc_state);
if let Some(search_query) = search_query.query {
if search_query.trim() != "" {
return handle_search_request(search_query, false, settings, state).await;
return handle_search_request(
search_query,
false,
settings,
state,
&client_ip
).await;
}
}
let result = get_ip_result(&address, &settings.lang, &state).await;
let result = get_ip_result(
&client_ip,
&settings.lang,
&settings.dns_resolver_id,
settings.dns_disable_self_lookup,
&client_ip,
&state,
).await;
let user_agent: Option<String> = match user_agent_header {
Some(TypedHeader(user_agent)) => Some(user_agent.to_string()),
@ -390,26 +437,26 @@ async fn handle_default_route(
state.templating_engine.render_view(
&settings,
&View::Index{
View::Index{
result: result,
user_agent: user_agent,
}
).await
)
}
static ASN_REGEX: LazyLock<Regex> = LazyLock::new(|| { Regex::new(r"^[Aa][Ss][Nn]?\s*(\d{1,7})$").unwrap() });
static VIA_REGEX: LazyLock<Regex> = LazyLock::new(|| { Regex::new(r"[Vv][Ii][Aa]\s+(\S+)").unwrap() });
async fn handle_search_request(
search_query: String,
this_should_have_been_an_ip: bool,
settings: TemplateSettings,
settings: QuerySettings,
arc_state: Arc<ServiceSharedState>,
client_ip: &IpAddr,
) -> Response {
let search_query = search_query.trim();
lazy_static!{
static ref ASN_REGEX: Regex = Regex::new(r"^[Aa][Ss][Nn]?\s*(\d{1,7})$").unwrap();
}
let mut search_query = search_query.trim().to_string();
let mut settings = settings;
//If someone asked for an asn, give an asn answer
if let Some(asn_cap) = ASN_REGEX.captures(&search_query) {
@ -418,57 +465,121 @@ async fn handle_search_request(
let state = Arc::clone(&arc_state);
return state.templating_engine.render_view(
&settings,
&View::Asn{asn: asn},
).await
View::Asn{asn: asn},
)
}
}
if let Some(via_cap) = VIA_REGEX.captures(&search_query) {
if let Some(via) = via_cap.get(1) {
let state = Arc::clone(&arc_state);
if state.dns_resolvers.contains_key(via.as_str()) {
settings.dns_resolver_id = via.as_str().into();
} else if let Some(alias) = state.dns_resolver_aliases.get(via.as_str()) {
settings.dns_resolver_id = alias.clone();
}
}
search_query = VIA_REGEX.replace(&search_query,"").trim().to_string();
}
// Try to interpret as an IP-Address
if let Ok(address) = search_query.parse() {
return handle_ip_request(address, settings, arc_state).await;
return handle_ip_request(address, settings, arc_state, client_ip).await;
}
// Fall back to treating it as a hostname
return handle_dig_request(
search_query.to_string(), settings, arc_state,
search_query,
settings,
arc_state,
!this_should_have_been_an_ip,
).await
}
async fn handle_ip_route_with_path(
Extension(settings): Extension<TemplateSettings>,
async fn handle_dns_resolver_route(
Extension(settings): Extension<QuerySettings>,
State(arc_state): State<Arc<ServiceSharedState>>,
) -> Response {
let state = Arc::clone(&arc_state);
state.templating_engine.render_view(
&settings,
View::DnsResolverList,
)
}
async fn handle_dns_resolver_route_with_path(
Extension(settings): Extension<QuerySettings>,
State(arc_state): State<Arc<ServiceSharedState>>,
extract::Path(query): extract::Path<String>,
) -> Response {
if let Ok(address) = query.parse() {
return handle_ip_request(address, settings, arc_state).await
let state = Arc::clone(&arc_state);
if let Some(resolver) = state.config.dns.resolver.get(query.as_str()) {
state.templating_engine.render_view(
&settings,
View::DnsResolver{ config: resolver.clone() },
)
} else {
return handle_search_request(query, true, settings, arc_state).await;
state.templating_engine.render_view(
&settings,
View::NotFound,
)
}
}
async fn handle_ip_route_with_path(
Extension(settings): Extension<QuerySettings>,
State(arc_state): State<Arc<ServiceSharedState>>,
extract::Path(query): extract::Path<String>,
SecureClientIp(client_ip): SecureClientIp
) -> Response {
if let Ok(address) = query.parse() {
return handle_ip_request(address, settings, arc_state, &client_ip).await
} else {
return handle_search_request(query, true, settings, arc_state, &client_ip).await;
}
}
async fn handle_ip_request(
address: IpAddr,
settings: TemplateSettings,
settings: QuerySettings,
arc_state: Arc<ServiceSharedState>,
client_ip: &IpAddr,
) -> Response {
let state = Arc::clone(&arc_state);
let result = get_ip_result(&address, &settings.lang, &state).await;
let result = get_ip_result(
&address,
&settings.lang,
&settings.dns_resolver_id,
settings.dns_disable_self_lookup,
client_ip,
&state).await;
state.templating_engine.render_view(
&settings,
&View::Ip{result: result}
).await
View::Ip{result: result}
)
}
async fn get_ip_result(
address: &IpAddr,
lang: &String,
dns_resolver_name: &Arc<str>,
dns_disable_self_lookup: bool,
client_ip: &IpAddr,
state: &ServiceSharedState,
) -> IpResult {
let mut reverse_dns_disabled_for_privacy = false;
if state.config.dns.allow_reverse_lookup {
if address == client_ip && dns_disable_self_lookup {
reverse_dns_disabled_for_privacy = true;
}
}
let ip_info = AddressInfo::new(&address);
if !(ip_info.scope == AddressScope::Global || ip_info.scope == AddressScope::Shared) || ip_info.cast != AddressCast::Unicast {
@ -479,16 +590,21 @@ async fn get_ip_result(
asn: None,
location: None,
ip_info: ip_info,
used_dns_resolver: None,
reverse_dns_disabled_for_privacy: reverse_dns_disabled_for_privacy,
}
}
}
// do reverse lookup
let hostname = if state.config.dns.allow_reverse_lookup {
simple_dns::reverse_lookup(&state.dns_resolver, &address).await
} else {
None
};
let mut hostname: Option<String> = None;
let mut used_dns_resolver: Option<Arc<str>> = None;
if state.config.dns.allow_reverse_lookup && !reverse_dns_disabled_for_privacy {
if let Some(dns_resolver) = &state.dns_resolvers.get(dns_resolver_name) {
hostname = simple_dns::reverse_lookup(&dns_resolver, &address).await;
used_dns_resolver = Some(dns_resolver_name.clone());
}
}
// asn lookup
let asn_result = state.asn_db.query_asn_for_ip(address);
@ -500,28 +616,26 @@ async fn get_ip_result(
);
// filter reverse lookup
let final_hostname = match hostname {
Some(name) => {
if match_domain_hidden_list(&name, &state.config.dns.hidden_suffixes) {
None
} else {
Some(name.to_owned())
}
},
None => None,
};
if let Some(name) = &hostname {
if match_domain_hidden_list(&name, &state.config.dns.hidden_suffixes) {
hostname = None;
used_dns_resolver = None;
}
}
IpResult{
address: *address,
hostname: final_hostname,
hostname: hostname,
asn: asn_result,
location: location_result,
ip_info: ip_info,
used_dns_resolver: used_dns_resolver,
reverse_dns_disabled_for_privacy: reverse_dns_disabled_for_privacy,
}
}
async fn handle_dig_route_with_path(
Extension(settings): Extension<TemplateSettings>,
Extension(settings): Extension<QuerySettings>,
State(arc_state): State<Arc<ServiceSharedState>>,
extract::Path(name): extract::Path<String>,
) -> Response {
@ -530,39 +644,84 @@ async fn handle_dig_route_with_path(
async fn handle_dig_request(
dig_query: String,
settings: TemplateSettings,
settings: QuerySettings,
arc_state: Arc<ServiceSharedState>,
do_full_lookup: bool,
) -> Response {
let state = Arc::clone(&arc_state);
let dig_result = get_dig_result(&dig_query, &state, do_full_lookup).await;
let dig_result = get_dig_result(
&dig_query,
&settings.dns_resolver_id,
&state,
do_full_lookup
).await;
state.templating_engine.render_view(
&settings,
&View::Dig{ query: dig_query, result: dig_result}
).await
View::Dig{ query: dig_query, result: dig_result}
)
}
async fn get_dig_result(
dig_query: &String,
state: &ServiceSharedState,
do_full_lookup: bool,
dig_query: &String,
dns_resolver_name: &Arc<str>,
state: &ServiceSharedState,
do_full_lookup: bool,
) -> DigResult {
let name = &dig_query.trim().trim_end_matches(".").to_string();
if match_domain_hidden_list(&name, &state.config.dns.hidden_suffixes) {
Default::default()
let idna_name = IdnaName::from_string(&name);
if let Some(dns_resolver) = state.dns_resolvers.get(dns_resolver_name) {
if let Ok(domain_name) = Name::from_str_relaxed(name.to_owned()+".") {
if match_domain_hidden_list(&name, &state.config.dns.hidden_suffixes) {
// Try to hide the fact that we didn't do dns resolution at all
// We resolve example.org as basic avoidance of timing sidechannels.
// WARNING: this timing sidechannel avoidance is very crude.
simple_dns::lookup(
&dns_resolver,
&Name::from_ascii("example.org.").expect("Static Dummy Name"),
do_full_lookup).await;
return DigResult {
records: DnsLookupResult{ nxdomain: true , ..Default::default() },
idn: idna_name,
partial_lookup: !do_full_lookup,
used_dns_resolver: dns_resolver_name.clone(),
}
} else {
return DigResult {
records: simple_dns::lookup(
&dns_resolver,
&domain_name,
do_full_lookup).await,
idn: idna_name,
partial_lookup: !do_full_lookup,
used_dns_resolver: dns_resolver_name.clone(),
}
}
} else {
// Invalid domain name
return DigResult {
records: DnsLookupResult{
invalid_name: true,
.. Default::default()
},
idn: idna_name,
partial_lookup: !do_full_lookup,
used_dns_resolver: dns_resolver_name.clone(),
}
}
} else {
let idna_name = IdnaName::from_string(&name);
DigResult {
records: simple_dns::lookup(
&state.dns_resolver,
&(idna_name.idn.clone().unwrap_or(name.to_owned())+"."),
do_full_lookup).await,
// Unknown resolver name
return DigResult {
records: DnsLookupResult{
unkown_resolver: true,
.. Default::default()
},
idn: idna_name,
partial_lookup: !do_full_lookup,
used_dns_resolver: "unkown_resolver".into(),
}
}
}

View File

@ -1,5 +1,6 @@
use axum_client_ip::SecureClientIp;
use axum::{
body::Body,
extract::Extension,
http::{
Request,
@ -40,11 +41,11 @@ pub fn build_rate_limiting_state(
Extension(arc_limiter)
}
pub async fn rate_limit_middleware<B>(
pub async fn rate_limit_middleware(
SecureClientIp(address): SecureClientIp,
Extension(arc_limiter): Extension<Arc<SimpleRateLimiter<IpAddr>>>,
req: Request<B>,
next: Next<B>
req: Request<Body>,
next: Next
) -> Response {
let limiter = Arc::clone(&arc_limiter);

42
src/settings.rs Normal file
View File

@ -0,0 +1,42 @@
use serde::{Deserialize,Serialize};
use lib_humus::HtmlTextJsonFormat;
use lib_humus::HumusQuerySettings;
use std::sync::Arc;
/* Response format */
pub type ResponseFormat = HtmlTextJsonFormat;
/* Query and Template Settings */
#[derive(Deserialize, Serialize, Clone)]
pub struct QuerySettings {
pub format: ResponseFormat,
pub lang: String,
pub available_dns_resolvers: Vec<Selectable>,
pub dns_resolver_id: Arc<str>,
pub dns_disable_self_lookup: bool,
}
#[derive(Deserialize, Serialize, Clone)]
pub struct Selectable {
pub id: Arc<str>,
pub name: Arc<str>,
pub weight: i32,
}
impl HumusQuerySettings<ResponseFormat> for QuerySettings {
fn initalize_template_context(&self, context: &mut tera::Context) {
context.insert("language", &self.lang);
context.insert("dns_resolvers", &self.available_dns_resolvers);
context.insert("dns_resolver_id", &self.dns_resolver_id);
}
fn get_format(&self) -> ResponseFormat {
self.format.clone()
}
}

View File

@ -1,20 +1,20 @@
/*
* This module wraps the trust_dns_resolver library
* to generate results thaat are ready for serializing
* or templating.
* It does not aim to be reusable for any other purpose,
* the trust_dns_resolver library already does that.
*/
use trust_dns_proto::op::response_code::ResponseCode;
use trust_dns_proto::rr::{
//! This module wraps the hickory_resolver library
//! to generate results thaat are ready for serializing
//! or templating.
//! It does not aim to be reusable for any other purpose,
//! the hickory_resolver library already does that.
use hickory_proto::op::response_code::ResponseCode;
use hickory_proto::rr::{
RData,
record_type::RecordType,
};
use trust_dns_resolver::{
use hickory_resolver::{
error::ResolveError,
error::ResolveErrorKind,
lookup::Lookup,
Name,
TokioAsyncResolver,
};
@ -27,20 +27,22 @@ use std::net::IpAddr;
#[derive(serde::Deserialize, serde::Serialize, Default, Clone)]
pub struct DnsLookupResult {
a: Option<Vec<IpAddr>>,
aaaa: Option<Vec<IpAddr>>,
aname: Option<Vec<String>>,
cname: Option<Vec<String>>,
mx: Option<Vec<MxRecord>>,
ns: Option<Vec<String>>,
soa: Option<Vec<SoaRecord>>,
txt: Option<Vec<String>>,
srv: Option<Vec<SrvRecord>>,
caa: Option<Vec<String>>,
other_error: bool,
dns_error: bool,
nxdomain: bool,
timeout: bool,
pub a: Option<Vec<IpAddr>>,
pub aaaa: Option<Vec<IpAddr>>,
pub aname: Option<Vec<String>>,
pub cname: Option<Vec<String>>,
pub mx: Option<Vec<MxRecord>>,
pub ns: Option<Vec<String>>,
pub soa: Option<Vec<SoaRecord>>,
pub txt: Option<Vec<String>>,
pub srv: Option<Vec<SrvRecord>>,
pub caa: Option<Vec<String>>,
pub other_error: bool,
pub dns_error: bool,
pub nxdomain: bool,
pub timeout: bool,
pub invalid_name: bool,
pub unkown_resolver: bool,
}
#[derive(serde::Deserialize, serde::Serialize, Clone, PartialEq)]
@ -118,9 +120,9 @@ pub fn set_default_if_none<T>(opt_vec: &mut Option<Vec<T>>) {
pub fn add_record_to_lookup_result(result: &mut DnsLookupResult, record: &RData){
match record {
RData::AAAA(address) => opush(&mut result.aaaa, std::net::IpAddr::V6(*address)),
RData::AAAA(aaaa) => opush(&mut result.aaaa, std::net::IpAddr::V6(aaaa.0)),
RData::ANAME(aname) => opush(&mut result.aname, aname.to_string()),
RData::A(address) => opush(&mut result.a, std::net::IpAddr::V4(*address)),
RData::A(a) => opush(&mut result.a, std::net::IpAddr::V4(a.0)),
RData::CAA(caa) => opush(&mut result.caa, caa.to_string()),
RData::CNAME(cname) => opush(&mut result.cname, cname.to_string()),
RData::MX(mx) => opush(&mut result.mx, MxRecord{
@ -171,8 +173,14 @@ pub fn integrate_lookup_result(dig_result: &mut DnsLookupResult, lookup_result:
RecordType::TXT => set_default_if_none(&mut dig_result.txt),
_ => { /* This should not happen */ },
};
for record in lookup.iter() {
add_record_to_lookup_result(dig_result, record);
let name = lookup.query().name();
for record in lookup.record_iter() {
if name == record.name() {
if let Some(data) = record.data() {
add_record_to_lookup_result(dig_result, data);
}
}
//TODO: handle additional responses
}
},
Err(e) => {
@ -210,7 +218,7 @@ pub fn integrate_lookup_result(dig_result: &mut DnsLookupResult, lookup_result:
// records will be fetched.
pub async fn lookup(
resolver: &TokioAsyncResolver,
name: &String,
name: &Name,
do_full_lookup: bool,
) -> DnsLookupResult {
let (
@ -219,10 +227,10 @@ pub async fn lookup(
cname_lookup_res,
aname_lookup_res
) = join!(
resolver.lookup(name, RecordType::A),
resolver.lookup(name, RecordType::AAAA),
resolver.lookup(name, RecordType::CNAME),
resolver.lookup(name, RecordType::ANAME),
resolver.lookup(name.clone(), RecordType::A),
resolver.lookup(name.clone(), RecordType::AAAA),
resolver.lookup(name.clone(), RecordType::CNAME),
resolver.lookup(name.clone(), RecordType::ANAME),
);
// initlize an empty lookup result
@ -243,12 +251,12 @@ pub async fn lookup(
srv_lookup_res,
txt_lookup_res
) = join!(
resolver.lookup(name, RecordType::MX),
resolver.lookup(name, RecordType::NS),
resolver.lookup(name, RecordType::SOA),
resolver.lookup(name, RecordType::CAA),
resolver.lookup(name, RecordType::SRV),
resolver.lookup(name, RecordType::TXT),
resolver.lookup(name.clone(), RecordType::MX),
resolver.lookup(name.clone(), RecordType::NS),
resolver.lookup(name.clone(), RecordType::SOA),
resolver.lookup(name.clone(), RecordType::CAA),
resolver.lookup(name.clone(), RecordType::SRV),
resolver.lookup(name.clone(), RecordType::TXT),
);
integrate_lookup_result(&mut dig_result, mx_lookup_res);

View File

@ -1,146 +0,0 @@
/*
* This is the echoip-slatecave templating engine.
* It wraps around tera in is specialized for echoip-slatecave.
*/
use axum::{
http::StatusCode,
response::Html,
response::IntoResponse,
response::Response,
response::Json,
};
use tera::Tera;
use toml::Table;
use crate::DigResult;
use crate::IpResult;
/* Response format */
#[derive(serde::Deserialize, serde::Serialize, Clone, Copy)]
pub enum ResponseFormat {
#[serde(rename="text/plain", alias="text")]
TextPlain,
#[serde(rename="text/html", alias="html")]
TextHtml,
#[serde(rename="application/json", alias="json")]
ApplicationJson,
}
impl ToString for ResponseFormat {
fn to_string(&self) -> String {
match self {
ResponseFormat::TextPlain => "text/plain",
ResponseFormat::TextHtml => "text/html",
ResponseFormat::ApplicationJson => "application/json",
}.to_string()
}
}
impl ResponseFormat {
fn to_file_extension(&self) -> String {
match self {
ResponseFormat::TextPlain => ".txt",
ResponseFormat::TextHtml => ".html",
ResponseFormat::ApplicationJson => ".json",
}.to_string()
}
}
/* Template Settings */
#[derive(serde::Deserialize, serde::Serialize, Clone)]
pub struct TemplateSettings {
pub format: ResponseFormat,
pub lang: String,
}
/* The echoip view */
#[derive(serde::Deserialize, serde::Serialize, Clone)]
#[serde(untagged)]
pub enum View {
Asn { asn: u32 },
Dig { query: String, result: DigResult },
Index { result: IpResult, user_agent: Option<String> },
Ip { result: IpResult },
Message{ title: String, message: String },
#[serde(rename="404")]
NotFound,
}
impl View {
pub fn template_name(&self) -> String {
match self {
View::Asn{..} => "asn",
View::Dig{..} => "dig",
View::Index{..} => "index",
View::Ip{..} => "ip",
View::Message{..} => "message",
View::NotFound => "404",
}.to_string()
}
}
/* The engine itself */
#[derive(Clone)]
pub struct Engine {
pub tera: Tera,
pub template_config: Option<Table>,
}
impl Engine {
pub async fn render_view(
&self,
settings: &TemplateSettings,
view: &View,
) -> Response {
let mut response = match settings.format {
ResponseFormat::TextHtml | ResponseFormat::TextPlain => {
let template_name = view.template_name();
let mut context = tera::Context::new();
context.insert("view", &template_name);
//intented for shared macros
context.insert("format", &settings.format.to_string());
context.insert("language", &settings.lang);
context.insert("data", &view);
context.insert("extra", &self.template_config);
match self.tera.render(&(template_name+&settings.format.to_file_extension()), &context) {
Ok(text) =>
match settings.format {
ResponseFormat::TextHtml => Html(text).into_response(),
_ => text.into_response(),
}
Err(e) => {
println!("There was an error while rendering index.html: {e:?}");
(
StatusCode::INTERNAL_SERVER_ERROR,
"Template error, contact owner or see logs.\n"
).into_response()
}
}
}
//TODO: Plain Text should have its own matcher
ResponseFormat::ApplicationJson => {
match view {
View::Dig{result, ..} => {
Json(result).into_response()
},
View::Index{result, ..} | View::Ip{result, ..} => {
Json(result).into_response()
},
_ => Json(view).into_response(),
}
}
};
match view {
View::NotFound => *response.status_mut() = StatusCode::NOT_FOUND,
_ => {},
}
response
}
}

80
src/view.rs Normal file
View File

@ -0,0 +1,80 @@
use axum::http::status::StatusCode;
use axum::Json;
use axum::response::IntoResponse;
use axum::response::Response;
use axum_extra::extract::cookie::Cookie;
use axum_extra::extract::cookie;
use lib_humus::HumusView;
use crate::DigResult;
use crate::IpResult;
use crate::config::DnsResolverConfig;
use crate::settings::QuerySettings;
use crate::settings::ResponseFormat;
#[derive(serde::Serialize, Clone)]
#[serde(untagged)]
pub enum View {
Asn { asn: u32 },
Dig { query: String, result: DigResult },
DnsResolver{ config: DnsResolverConfig },
DnsResolverList,
Index { result: IpResult, user_agent: Option<String> },
Ip { result: IpResult },
Message{ title: String, message: String },
#[serde(rename="404")]
NotFound,
}
impl HumusView<QuerySettings, ResponseFormat> for View {
fn get_template_name(&self) -> String {
match self {
View::Asn{..} => "asn",
View::Dig{..} => "dig",
View::DnsResolver{..} => "dns_resolver",
View::DnsResolverList => "dns_resolver_list",
View::Index{..} => "index",
View::Ip{..} => "ip",
View::Message{..} => "message",
View::NotFound => "404",
}.to_string()
}
fn get_status_code(&self, _: &QuerySettings) -> StatusCode {
match self {
Self::NotFound => StatusCode::NOT_FOUND,
_ => StatusCode::OK,
}
}
fn get_cookie_header(&self, settings: &QuerySettings) -> Option<String> {
Some(
Cookie::build(Cookie::new("dns_resolver",settings.dns_resolver_id.to_string()))
.path("/")
.same_site(cookie::SameSite::Strict)
.build()
.to_string()
)
}
fn get_api_response(self, settings: &QuerySettings) -> Response {
match self {
Self::Dig{result, ..} => {
Json(result).into_response()
},
Self::Index{result, ..} | Self::Ip{result, ..} => {
Json(result).into_response()
},
Self::DnsResolverList => {
Json(settings.available_dns_resolvers.clone()).into_response()
},
Self::DnsResolver{ config } => {
Json(config).into_response()
}
_ => Json(self).into_response(),
}
}
}

View File

@ -8,6 +8,7 @@
<li><a href="{{ extra.base_url}}">The homepage</a></li>
<li>The <code>/ip/</code> or <code>/dig/</code> endpoints.</li>
<li><a href="{{ extra.base_url }}/ua">The <code>/ua</code> endpoint wich just displays your user agent.</a></li>
<li><a href="{{ extra.base_url }}/dns_resolver">The list of configured dns resolvers.</a></li>
</ul>
</section>
{% endblock %}

View File

@ -5,6 +5,9 @@
<meta charset="utf-8">
<title>{% block title %}{{ extra[view].title | default(value="…") }}{% endblock %} | {{extra.site_name|default(value="echoip")}}</title>
<meta content="width=device-width, initial-scale=1" name="viewport">
<meta name="color-scheme" content="echoip-slatecave <https://codeberg.org/slatian/service.echoip-slatecave>">
{% block robots_meta %}
{% endblock robots_meta %}
<!-- Open-Graph -->
{% block metadata %}
<meta name="description" property="og:description" content="{% block description %}{{ extra[view].description | default(value="One of the best echoip services") | escape_xml }}{% endblock %}" />
@ -24,12 +27,23 @@
<body>
<header>
<nav>
<a href="{{ extra.base_url }}" class="sitename">{{extra.site_name|default(value="echoip")}}</a>
<a href="{{ extra.base_url }}" class="sitename">
{%- if extra.display_icon -%}
<img src="{{extra.display_icon}}" alt="">
{%- endif -%}
{{extra.site_name|default(value="echoip")}}</a>
<form class="search" method="GET" action="{{ extra.base_url }}">
<input type="search" name="query" autocomplete="on" maxlength="260"
title="Search for an IP-Adress, Domain-Name, or ASN."
placeholder="1.2.3.4, 2001::1:2:3:4, example.org, AS1234"
value="{% if view == "dig" %}{{ data.query }}{% elif view == "ip" %}{{ data.result.address }}{% elif view == "asn"%}AS{{ data.asn }}{% endif %}"/>
{% if dns_resolvers | length > 1 %}
<select name="dns" title="DNS Resolver">
{% for r in dns_resolvers %}
<option value="{{ r.id }}" {% if r.id == dns_resolver_id %}selected{%endif%}>{{ r.name }}</option>
{% endfor %}
</select>
{% endif %}
<input type="submit" value="Query"/>
</form>
</nav>

View File

@ -2,9 +2,11 @@
{% import "helpers.html" as helper %}
{% import "links.html" as links %}
{% block robots_meta %}<meta name="robots" content="noindex,nofollow">{% endblock %}
{% block title %}dig {{ data.query }}{% endblock %}
{% block og_title %}dig {{ data.query }}{% endblock %}
{% block h1 %}dig <code>{{ helper::breadcrumb_domain(extra=extra, name=data.query) }}</code>{% endblock %}
{% block h1 %}dig <code>{{ helper::breadcrumb_domain(extra=extra, name=data.query) }}</code> <small>via <a href="{{extra.base_url}}/dns_resolver/{{data.result.used_dns_resolver}}">{{data.result.used_dns_resolver}}</a></small>{% endblock %}
{% block og_path %}/dig/{{ data.query | urlencode_strict }}{% endblock %}
@ -26,8 +28,16 @@
<section>
<h2>DNS Records</h2>
{% if r.nxdomain %}
<p class="error box">Our DNS-Server claims that this domain doesn't exist, you shouldn't see any results below.</p>
{% set show_nonpresent = true %}
{% if r.unkown_resolver %}
<p class="error box">The resolver you chose is not one of the available ones, if you can reproduce this error by just using the UI <a href="https://codeberg.org/slatian/service.echoip-slatecave/issues/new">please report it</a>.</p>
{% set show_nonpresent = false %}
{% elif r.invalid_name %}
<p class="error box">This domain name does not conform to <a href="https://www.rfc-editor.org/info/std3">the dns specification (std3)</a> rules and was therefore not resolved.</p>
{% set show_nonpresent = false %}
{% elif r.nxdomain %}
<p class="error box">The DNS-Server claims that this domain doesn't exist, there shouldn't be any results.</p>
{% set show_nonpresent = false %}
{% elif r.timeout %}
<p class="error box">There was at least one timeout error while resolving this domain, the results below are incomplete.</p>
{% elif r.other_error %}
@ -66,7 +76,7 @@
<li>{{ helper::ip(extra=extra, ip=address) }}</li>
{% endfor %}
</ul>
{% else %}
{% elif show_nonpresent %}
<p>No <code>A</code> (IPv4) Records.</p>
{% endif %}
@ -77,7 +87,7 @@
<li>{{ helper::ip(extra=extra, ip=address) }}</li>
{% endfor %}
</ul>
{% else %}
{% elif show_nonpresent %}
<p>No <code>AAAA</code> (IPv6) Records.</p>
{% endif %}
@ -90,7 +100,7 @@
<li>{{ helper::dig(extra=extra, name=mx.exchange, fqdn=true, prefix=mx.preference) }}</li>
{% endfor %}
</ul>
{% else %}
{% elif show_nonpresent %}
<p id="mx">No <code>MX</code> (Mail Exchange) records.</p>
{% endif %}
@ -116,7 +126,7 @@
</dl></li>
{% endfor %}
</ul>
{% else %}
{% elif show_nonpresent %}
<p id="soa">No <code>SOA</code> records.</p>
{% endif %}
@ -129,7 +139,7 @@
<li>{{ helper::dig(extra=extra, name=ns) }}</li>
{% endfor %}
</ul>
{% else %}
{% elif show_nonpresent %}
<p id="ns">No <code>NS</code> (Name Server) records.</p>
{% endif %}
@ -141,7 +151,7 @@
<li><code>{{caa}}</code></li>
{% endfor %}
</ul>
{% else %}
{% elif show_nonpresent %}
<p id="caa">No <code>CAA</code> (<a target="_blank" href="https://de.wikipedia.org/wiki/DNS_Certification_Authority_Authorization">Certification Authority Authorization</a>) records.</p>
{% endif %}
@ -152,7 +162,7 @@
<li><code>{{txt}}</code></li>
{% endfor %}
</ul>
{% else %}
{% elif show_nonpresent %}
<p id="txt">No <code>TXT</code> records.</p>
{% endif %}
@ -172,7 +182,7 @@
</dl></li>
{% endfor %}
</ul>
{% else %}
{% elif show_nonpresent %}
<p id="srv">No <code>SRV</code> records.</p>
<p><code>SRV</code> or Service records usually live on their own subdomains like {{ helper::dig(extra=extra, name="_xmpp-client._tcp."~data.query) }}.
{% endif %}

View File

@ -2,8 +2,9 @@
{% block path %}dig/{{ data.query | urlencode_strict }}{% endblock %}
{% block content -%}
# dig {{data.query}}
{% set r = data.result.records %}
{%- block content -%}
# dig {{data.query}} via {{ data.result.used_dns_resolver }}
{% if data.result.idn -%}
{%- set idn = data.result.idn -%}
@ -25,8 +26,17 @@ Your IDN would decode to
{% set r = data.result.records -%}
## DNS Records
{% if r.nxdomain %}
Our DNS-Server claims that this domain doesn't exist, you shouldn't see any results below.
{% if r.unkown_resolver %}
{%- set show_nonpresent = false %}
The resolver you chose is not one of the available ones.
=> {{ extra.base_url }}/dns_resolver
{% elif r.invalid_name %}
{%- set show_nonpresent = false %}
This domain name does not conform to the dns specification (std3) rules and was therefore not resolved.
=> https://www.rfc-editor.org/info/std3
{% elif r.nxdomain %}
{%- set show_nonpresent = false %}
Our DNS-Server claims that this domain doesn't exist, there shouldn't be any results.
{%- elif r.timeout -%}
There was at least one timeout error while resolving this domain, the results below are incomplete.
{%- elif r.other_error -%}
@ -61,7 +71,7 @@ A (IPv4) records:
{% for address in r.a -%}
* {{ address }}
{% endfor %}
{%- else %}
{%- elif show_nonpresent %}
No A (IPv4) Records.
{% endif -%}
@ -70,7 +80,7 @@ AAAA (IPv6) records:
{% for address in r.aaaa -%}
* {{ address }}
{% endfor %}
{%- else %}
{%- elif show_nonpresent %}
No AAAA (IPv6) Records.
{% endif -%}
@ -81,7 +91,7 @@ MX (Mail Exchange) records:
{% for mx in r.mx | sort(attribute="preference") | reverse -%}
* {{ mx.preference }} {{ mx.exchange }}
{% endfor %}
{%- else %}
{%- elif show_nonpresent %}
No MX (Mail Exchange) records.
{% endif %}
@ -96,7 +106,7 @@ SOA (Source Of Authority) records:
* expire: {{soa.expire / 3600 | round(precision=2)}}h
* minimum: {{soa.minimum / 60 | round(precision=2)}}m TTL
{% endfor %}
{%- else %}
{%- elif show_nonpresent %}
No SOA (Source Of Authority) records.
{% endif %}
@ -105,7 +115,7 @@ NS (Name Server) records:
{% for ns in r.ns -%}
* {{ns}}
{% endfor %}
{%- else %}
{%- elif show_nonpresent %}
No NS (Name Server) records.
{% endif %}
@ -114,7 +124,7 @@ CAA (Certification Authority Authorization) records:
{% for caa in r.caa -%}
* {{caa}}
{% endfor %}
{%- else %}
{%- elif show_nonpresent %}
No CAA (Certification Authority Authorization) records.
{% endif %}
@ -123,7 +133,7 @@ TXT records:
{% for txt in r.txt -%}
* {{txt}}
{% endfor %}
{%- else %}
{%- elif show_nonpresent %}
No TXT records.
{% endif %}
@ -135,7 +145,7 @@ SRV records:
* Port: {{srv.port}}
* Target: {{srv.target}}
{% endfor %}
{%- else %}
{%- elif show_nonpresent %}
No SRV records.
SRV or Service records usually live on their own subdomains like {{ "_xmpp-client._tcp."~data.query }}.

View File

@ -0,0 +1,52 @@
{% extends "base.html" %}
{% import "helpers.html" as helper %}
{% block title %}{{ data.config.display_name }}{% endblock %}
{% block h1 %}DNS Resolver: {{ data.config.display_name }}{% endblock %}
{% block content %}
{%- set c = data.config %}
<section>
<h2>Connection Configuration</h2>
<dl>
<dt>Address{% if c.servers | length > 1 %}es{% endif %}</dt>
{% if c.servers | length > 0 %}
{%- for a in c.servers %}
{% if a is matching("^\[") %}
{% set ip = a | split(pat="]") | first | trim_start_matches(pat="[") %}
{% else %}
{% set ip = a | split(pat=":") | first %}
{% endif %}
<dd>{{ helper::ip(extra=extra, ip=ip, text=a) }}</dd>
{%- endfor %}
{%- else %}
<dd>None Configured</dd>
{%- endif %}
<dt>Protocol</dt>
<dd>{{ c.protocol }}</dd>
{%- if c.tls_dns_name %}
<dt>DNS Name</dt>
<dd>{{ helper::dig(extra=extra, name=c.tls_dns_name) }}</dd>
{%- endif %}
</dl>
{%- if c.info_url %}
<p class="button-paragraph"><a href="{{c.info_url}}">More about the {{c.display_name}} DNS Server <small>(external link)</small></a></p>
{%- endif %}
</section>
{%- if c.aliases | length > 0 %}
<section>
<h2>Aliases</h2>
<ul>
{%- for a in c.aliases %}
<li>{{a}}</li>
{%- endfor %}
</ul>
<p class="box hint">You can use this DNS server by typing <code>via {{c.aliases | first }}</code> {% if c.aliases | length > 1 %}(or any other alias){% endif %} in the searchfield.</p>
</section>
<p><a href="{{extra.base_url}}/dns_resolver">Back to DNS Resolver list</a></p>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,30 @@
# DNS Resolver: {{ data.config.display_name }}
{%- set c = data.config %}
{% if c.servers | length == 1 %}
Address: {{ c.servers | first }}
{% elif c.servers | length > 1 %}
Addresses:
{%- for a in c.servers %}
* {{a}}
{%- endfor %}
{%- else %}
Address: None Configured
{%- endif %}
Protocol: {{ c.protocol }}
{%-if c.tls_dns_name %}
DNS Name: {{ c.tls_dns_name }}
{%- endif %}
{%- if c.aliases | length == 1 %}
Alias: {{ c.aliases | first }}
{%- elif c.aliases | length > 1 %}
Aliases:
{%- for a in c.aliases %}
* {{a}}
{%- endfor %}
{%- endif %}
{%- if c.info_url %}
=> {{ c.info_url }}
{%- endif %}

View File

@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block title %}List of DNS Servers{% endblock %}
{% block h1 %}List of DNS Servers{% endblock %}
{% block content %}
<section>
<p>This is a list of DNS resolvers that are configured with this echoip service.</p>
<ul class="link-list">
{% for c in dns_resolvers %}
<li><a href="{{ extra.base_url }}/dns_resolver/{{c.id}}">{{c.name}}</a></li>
{% endfor %}
</ul>
<p>The reasons for them being here range from them being popular, privacy friendly, interesting, don't take the a server listed here as endorsement.</p>
</section>
{% endblock %}

View File

@ -0,0 +1,4 @@
# Here is a list of supported dns resolvers
{% for r in dns_resolvers %}
=> ./{{ r.id }} {{r.name}}
{%- endfor %}

View File

@ -8,15 +8,26 @@ base_url="http://localhost:3000"
stylesheet = "/style.css"
# URL to and mimetype of your favicon
# favicon = ""
# favicon_mimetype = "image/png"
favicon = "/icon_64.png"
favicon_mimetype = "image/png"
# favicon_mimetype = "image/svg+xml"
# favicon_mimetype = "image/jpeg"
# Icon to display next to the title
display_icon = "/icon_64.png"
# URLs to look up v4 and v6 addresses explicitly
# If you have not configured them, comment them out, the button will stay hidden
v4_url="http://v4.localhost:3000/"
v6_url="http://v6.localhost:3000/"
# Geolocation Attribution for MaxMind
#geo_attribution_html="The Geolocation and ASN information is provided by the GeoLite2 database created by <a href='https://www.maxmind.com/'>MaxMind</a>."
# Geolocation Attribution for DB-IP
#geo_attribution_html="The Geolocation and ASN information is provided by <a href='https://db-ip.com/'>DB-IP</a>."
[404]
# configure the 404 page, this is available for other pages too!
# Use the template name as the section name.

View File

@ -1,6 +1,6 @@
{% macro place_dl(place, label="", iso_code_prefix="") -%}
{%- if place -%}
{%- if format=="text/html" %}
{%- if format=="html" %}
{% if label %}<dt>{{label}}</dt>{% endif %}
<dd>{{place.name}} {% if place.iso_code%}({% if iso_code_prefix %}{{iso_code_prefix}}-{% endif %}{{place.iso_code}}){% endif %}</dd>
{% else -%}
@ -17,8 +17,8 @@
<a href="{{ self::dig_link(extra=extra, name=name) }}">{% if prefix %}{{ prefix }} {% endif %}{% if fqdn or name=="." %}{{ name }}{% else %}{{ name | trim_end_matches(pat=".") }}{% endif %}</a>
{% endmacro dig %}
{% macro ip(extra, ip) %}
<a href="{{ extra.base_url }}/ip/{{ ip | urlencode_strict | replace(from="%2e", to=".") | replace(from="%3a", to=":") | safe }}"><code>{{ ip }}</code></a>
{% macro ip(extra, ip, text=false, with_self_lookup=false) %}
<a href="{{ extra.base_url }}/ip/{{ ip | urlencode_strict | replace(from="%2e", to=".") | replace(from="%3a", to=":") | safe }}{% if with_self_lookup %}?dns_self_lookup=true{% endif %}"><code>{% if text %}{{ text }}{% else %}{{ ip }}{% endif %}</code></a>
{% endmacro dig %}
{% macro breadcrumb_domain(extra, name) %}
@ -31,7 +31,7 @@
{%- set_global i = i+1 -%}
{% endfor %}
{% endmacro breadcrumb_domain %}
{%- endmacro breadcrumb_domain %}
{% macro ip_info(ip_info) -%}
{{ip_info.scope | title}} {{ip_info.cast | title}} IPv{% if ip_info.is_v6_address %}6{% else %}4{% endif %}

View File

@ -1,6 +1,8 @@
{% extends "ip.html" %}
{% import "helpers.html" as helper %}
{% block robots_meta %}{# Allow indexing for landing page #}{% endblock %}
{% block title %}Your IP: {{ data.result.address }}{% endblock %}
{% block og_title %}What is my IP-Address?{% endblock %}
{% block h1 %}Your IPv{% if data.result.ip_info.is_v6_address %}6{% else %}4{% endif %}: <code>{{ data.result.address }}</code>{% endblock %}
@ -33,5 +35,7 @@
<section>
<h2>Did you know?</h2>
<p>If you share this site and the Link gets a preview. The IP-Address after the dash is the one of the machine that generated that preview.</p>
<p>This service exports a <a href="{{ extra.base_url }}/dns_resolver">list of dns resolvers it supports</a>, with configuration.</p>
<p>Every query that can output html can also output json or plain text using the <code>?format=json</code> or <code>?format=text</code> url parameters?</p>
</section>
{% endblock %}

View File

@ -2,6 +2,8 @@
{% import "helpers.html" as helper %}
{% import "links.html" as links %}
{% block robots_meta %}<meta name="robots" content="noindex,nofollow">{% endblock %}
{% block title %}{{ data.result.address }}{% endblock %}
{% block og_title %}Lookup {{ data.result.address }}{% endblock %}
{% block h1 %}Lookup <code>{{ data.result.address }}</code>{% endblock %}
@ -18,6 +20,9 @@
{% if r.hostname %}
<dt>Hostname</dt>
<dd>{{ helper::dig(extra=extra, name=r.hostname) }}</dd>
{% elif r.reverse_dns_disabled_for_privacy %}
<dt>Hostname</dt>
<dd>Lookup disabled by default: {{ helper::ip(ip=r.address, extra=extra, text="enable", with_self_lookup=true)}}</dd>
{% endif %}
{% if r.asn %}
<dt><abbr="Autonomous System Number">ASN</abbr></dt>
@ -31,43 +36,44 @@
{% if r.location %}
<section>
<h2>Geolocation</h2>
<dl>
{{ helper::place_dl(place=r.location.continent, label="Continent") }}
{{ helper::place_dl(place=r.location.country, label="Country") }}
{% if r.location.country.iso_code | default(value="") != r.location.registered_country.iso_code | default(value="") %}
{{ helper::place_dl(place=r.location.registered_country, label="Registered in") }}
{% endif %}
{% if r.location.country.iso_code | default(value="") != r.location.represented_country.iso_code | default(value="")%}
{{ helper::place_dl(place=r.location.represented_country, label="Represents") }}
{% endif %}
{% if r.location.subdivisions %}
{% for sd in r.location.subdivisions %}
{{ helper::place_dl(place=sd, label="Subdivision", iso_code_prefix=r.location.country.iso_code|default(value="")) }}
{% endfor %}
{% endif %}
{{ helper::place_dl(place=r.location.city, label="City") }}
{% if r.location.postal_code %}
<dt>Postal Code</dt>
<dd>{{r.location.postal_code}}</dd>
{% endif %}
{% if r.location.time_zone %}
<dt>Timezone</dt>
<dd>{{r.location.time_zone}}</dd>
{% endif %}
{% if r.location.accuracy %}
<dt>Accuracy</dt>
<dd>~{{r.location.accuracy}}km</dd>
{% endif %}
{% if r.location.coordinates %}
<dt>Coordinates</dt>
<dd><a target="_blank" href="{{ links::map_link(lat=r.location.coordinates.lat, lon=r.location.coordinates.lon)}}">lat: {{r.location.coordinates.lat}}, lon: {{r.location.coordinates.lon}}</a></dd>
{% endif %}
</dl>
<!--We have to put that there to comply with maxminds licensing-->
<p><small>
The GeoIP and ASN information is provided by the GeoLite2 database created by
<a target="_blank" href="https://www.maxmind.com">MaxMind</a>.
</small></p>
{% if extra.geo_attribution_html %}
<dl>
{{ helper::place_dl(place=r.location.continent, label="Continent") }}
{{ helper::place_dl(place=r.location.country, label="Country") }}
{% if r.location.country.iso_code | default(value="") != r.location.registered_country.iso_code | default(value="") %}
{{ helper::place_dl(place=r.location.registered_country, label="Registered in") }}
{% endif %}
{% if r.location.country.iso_code | default(value="") != r.location.represented_country.iso_code | default(value="")%}
{{ helper::place_dl(place=r.location.represented_country, label="Represents") }}
{% endif %}
{% if r.location.subdivisions %}
{% for sd in r.location.subdivisions %}
{{ helper::place_dl(place=sd, label="Subdivision", iso_code_prefix=r.location.country.iso_code|default(value="")) }}
{% endfor %}
{% endif %}
{{ helper::place_dl(place=r.location.city, label="City") }}
{% if r.location.postal_code %}
<dt>Postal Code</dt>
<dd>{{r.location.postal_code}}</dd>
{% endif %}
{% if r.location.time_zone %}
<dt>Timezone</dt>
<dd>{{r.location.time_zone}}</dd>
{% endif %}
{% if r.location.accuracy %}
<dt>Accuracy</dt>
<dd>~{{r.location.accuracy}}km</dd>
{% endif %}
{% if r.location.coordinates %}
<dt>Coordinates</dt>
<dd><a target="_blank" href="{{ links::map_link(lat=r.location.coordinates.lat, lon=r.location.coordinates.lon)}}">lat: {{r.location.coordinates.lat}}, lon: {{r.location.coordinates.lon}}</a></dd>
{% endif %}
</dl>
<p><small>{{extra.geo_attribution_html | safe}}</small></p>
{% else %}
<p><strong style="font-size: 2em">Please configure the <code>geo_attribution_html</code> key in the template extra configuration!</strong></p>
<p>The geolocation information will then become visible.</p>
{% endif %}
</section>
{% endif %}
{% block extra_content %}{% endblock %}

View File

@ -13,12 +13,19 @@
* Type of Address: {{ helper::ip_info(ip_info=r.ip_info) }}
{% if r.hostname -%}
* Hostname: {{ r.hostname }}
{%- elif r.reverse_dns_disabled_for_privacy %}
* Hostname: Lookup disabled by default
{%- endif %}
{% if r.asn -%}
* ASN: AS{{ r.asn.asn }}
* AS Name: {{r.asn.name}}
{%- endif -%}
{%- if r.reverse_dns_disabled_for_privacy %}
=> /ip/{{ data.result.address }}?dns_self_lookup=true Do a reverse DNS lookup
{% endif %}
{%- if r.location %}
## Geolocation
@ -52,7 +59,7 @@ lat: {{r.location.coordinates.lat}}, lon: {{r.location.coordinates.lon}}
=> {{ links::map_link(lat=r.location.coordinates.lat, lon=r.location.coordinates.lon)}}
{%- endif %}
The GeoIP and ASN information is provided by the GeoLite2 database created by MaxMind.
{{ extra.geo_attribution_html | default(value="Please configure the geo_attribution_html key in the template extra configuration.") | striptags }}
{% endif -%}
{%- block extra_content %}{% endblock -%}

View File

@ -16,12 +16,13 @@
{% macro domain_name_links(name) %}
<p>Look up <code>{{name}}</code></p>
<ul class="link-list">
<li><a target="_blank" href="https://www.shodan.io/domain/{{ name }}">… on shodan.io <small>(limited query's per day, wants an account)</small></a></li>
<li><a target="_blank" href="https://search.censys.io/search?resource=hosts&sort=RELEVANCE&per_page=25&virtual_hosts=EXCLUDE&q={{ name }}">… on search.censys.io <small>(10 query's per day, wants an account)</small></a></li>
<li><a target="_blank" href="https://www.virustotal.com/gui/domain/{{ name }}">… on virustotal.com</a></li>
<li><a target="_blank" href="https://observatory.mozilla.org/analyze/{{ name }}">… on the Mozilla Observatory (http and tls checks)</a></li>
<li><a target="_blank" href="https://internet.nl/site/{{ name }}">… on the Internet.nl Website test</a></li>
<li><a target="_blank" href="https://client.rdap.org/?type=domain&object={{ name }}">… on client.rdap.org <small>(a modern whois, make sure to allow xhr to 3rd parties)</small></a></li>
<li><a target="_blank" href="https://www.shodan.io/domain/{{ name | urlencode_strict }}">… on shodan.io <small>(limited query's per day, wants an account)</small></a></li>
<li><a target="_blank" href="https://search.censys.io/search?resource=hosts&sort=RELEVANCE&per_page=25&virtual_hosts=EXCLUDE&q={{ name | urlencode_strict }}">… on search.censys.io <small>(10 query's per day, wants an account)</small></a></li>
<li><a target="_blank" href="https://www.virustotal.com/gui/domain/{{ name | urlencode_strict }}">… on virustotal.com</a></li>
<li><a target="_blank" href="https://observatory.mozilla.org/analyze/{{ name | urlencode_strict }}">… on the Mozilla Observatory (http and tls checks)</a></li>
<li><a target="_blank" href="https://internet.nl/site/{{ name | urlencode_strict }}">… on the Internet.nl Website test</a></li>
<li><a target="_blank" href="https://client.rdap.org/?type=domain&object={{ name | urlencode_strict }}">… on client.rdap.org <small>(a modern whois, make sure to allow xhr to 3rd parties)</small></a></li>
<li><a target="_blank" href="https://crt.sh/?Identity={{ name | urlencode_strict }}&match==">… on crt.sh <small>(Certificate Transparancy Monitor)</small></a></li>
</ul>
{% endmacro domain_name_links %}

50
templates/static/icon.svg Normal file
View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="48"
height="48"
viewBox="0 0 48 48"
version="1.1"
id="svg1"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1">
<linearGradient
id="linearGradient8">
<stop
style="stop-color:#fb9a00;stop-opacity:1;"
offset="0"
id="stop8" />
<stop
style="stop-color:#884f00;stop-opacity:1;"
offset="0.49966338"
id="stop10" />
<stop
style="stop-color:#be8700;stop-opacity:1;"
offset="1"
id="stop9" />
</linearGradient>
<linearGradient
xlink:href="#linearGradient8"
id="linearGradient9"
x1="10.202637"
y1="35.241699"
x2="39.21582"
y2="12.833984"
gradientUnits="userSpaceOnUse" />
</defs>
<g
id="layer1">
<path
id="path2"
style="fill:url(#linearGradient9);fill-opacity:1;stroke-width:3.15427;stroke-linejoin:round;paint-order:stroke markers fill"
d="m 2,7 v 33.767595 l 1.586,0.0021 L 8.299716,45.41681 12.826,40.767584 H 46 V 7 Z" />
<path
id="rect1"
style="fill:#111111;stroke-width:3;stroke-linejoin:round;paint-order:stroke markers fill"
d="M 3 8 L 3 40 L 4.0019531 40 L 4 40.001953 L 8.2792969 44.205078 L 12.412109 40 L 45 40 L 45 8 L 3 8 z M 35.671875 11.712891 L 39.357422 11.712891 L 39.357422 36.287109 L 35.671875 36.287109 L 35.671875 17.033203 L 31.494141 21.363281 L 28.839844 18.804688 C 31.107109 16.462871 35.671875 11.712891 35.671875 11.712891 z M 8.6425781 21.542969 L 12.328125 21.542969 L 12.328125 25.228516 L 8.6425781 25.228516 L 8.6425781 21.542969 z M 20.927734 21.542969 L 24.615234 21.542969 L 24.615234 25.228516 L 20.927734 25.228516 L 20.927734 21.542969 z M 8.6425781 32.599609 L 12.328125 32.599609 L 12.328125 36.287109 L 8.6425781 36.287109 L 8.6425781 32.599609 z M 20.927734 32.599609 L 24.615234 32.599609 L 24.615234 36.287109 L 20.927734 36.287109 L 20.927734 32.599609 z " />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 950 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -380,6 +380,8 @@ a:visited {
color: var(--page-link-visited);
}
a.sitename { display: inline-block; }
h1, a.sitename {
margin: var(--heading-mg);
padding: var(--heading-pad);
@ -571,7 +573,7 @@ tr:hover, th {
/* Search form styling */
form.search > input {
form.search > input, form.search > select {
font-size: 1.2em;
font-weight: bold;
}
@ -597,3 +599,10 @@ form.search {
background: var(--button-bg);
}
/* Custom icon style for sitename*/
.sitename > img {
height: 1.2em;
padding: 0 0.3ch;
margin-bottom: -.2em;
}