Compare commits
38 Commits
dependabot
...
pmtu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90b9e81129 | ||
|
|
ffb0bec4da | ||
|
|
4d2b8787e0 | ||
|
|
d4831ad4a6 | ||
|
|
9e1b53a732 | ||
|
|
d0113849d6 | ||
|
|
7b25fdfee8 | ||
|
|
5ed6e82922 | ||
|
|
7dbd14df27 | ||
|
|
96d8b53338 | ||
|
|
2bd19640d9 | ||
|
|
1047508bd7 | ||
|
|
eb49306b80 | ||
|
|
43da9ddbb3 | ||
|
|
7fbc5c3c07 | ||
|
|
e03f545e07 | ||
|
|
942f1f2c0f | ||
|
|
baf566d7a5 | ||
|
|
6712adfe6b | ||
|
|
2e2e5f9df5 | ||
|
|
35e9b2365d | ||
|
|
2391c890b4 | ||
|
|
51fd46b58e | ||
|
|
906e7b5ee1 | ||
|
|
5428580b8f | ||
|
|
6c25ee53f1 | ||
|
|
b9051b02bf | ||
|
|
f0f3193c1c | ||
|
|
c0ebd180cb | ||
|
|
b6e873cf25 | ||
|
|
ccc2f306b9 | ||
|
|
5b1dc295fe | ||
|
|
00bc8bbbbb | ||
|
|
8bef380d8c | ||
|
|
9ad1907574 | ||
|
|
d83999d954 | ||
|
|
162d244865 | ||
|
|
e21d798f57 |
37
.github/ISSUE_TEMPLATE/provider.md
vendored
37
.github/ISSUE_TEMPLATE/provider.md
vendored
@@ -6,12 +6,35 @@ labels: ":bulb: New provider"
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
One of the following is required:
|
Important notes:
|
||||||
|
|
||||||
- Publicly accessible URL to a zip file containing the Openvpn configuration files
|
- There is no need to support both OpenVPN and Wireguard for a provider, but it's better to support both if possible
|
||||||
- Publicly accessible URL to a structured (JSON etc.) list of servers **and attach** an example Openvpn configuration file for both TCP and UDP
|
- We do **not** implement authentication to access servers information behind a login. This is way too time consuming unfortunately
|
||||||
|
- If it's not possible to support a provider natively, you can still use the [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
|
||||||
|
|
||||||
|
## For Wireguard
|
||||||
|
|
||||||
|
Wireguard can be natively supported ONLY if:
|
||||||
|
|
||||||
|
- the `PrivateKey` field value is the same across all servers for one user account
|
||||||
|
- the `Address` field value is:
|
||||||
|
- can be found in a structured (JSON etc.) list of servers publicly available; OR
|
||||||
|
- the same across all servers for one user account
|
||||||
|
- the `PublicKey` field value is:
|
||||||
|
- can be found in a structured (JSON etc.) list of servers publicly available; OR
|
||||||
|
- the same across all servers for one user account
|
||||||
|
- the `Endpoint` field value:
|
||||||
|
- can be found in a structured (JSON etc.) list of servers publicly available
|
||||||
|
- can be determined using a pattern, for example using country codes in hostnames
|
||||||
|
|
||||||
|
If any of these conditions are not met, Wireguard cannot be natively supported or there is no advantage compared to using a custom Wireguard configuration file.
|
||||||
|
|
||||||
|
If **all** of these conditions are met, please provide an answer for each of them.
|
||||||
|
|
||||||
|
## For OpenVPN
|
||||||
|
|
||||||
|
OpenVPN can be natively supported ONLY if one of the following can be provided, by preference in this order:
|
||||||
|
|
||||||
|
- Publicly accessible URL to a structured (JSON etc.) list of servers **and attach** an example Openvpn configuration file for both TCP and UDP; OR
|
||||||
|
- Publicly accessible URL to a zip file containing the Openvpn configuration files; OR
|
||||||
- Publicly accessible URL to the list of servers **and attach** an example Openvpn configuration file for both TCP and UDP
|
- Publicly accessible URL to the list of servers **and attach** an example Openvpn configuration file for both TCP and UDP
|
||||||
|
|
||||||
If the list of servers requires to login **or** is hidden behind an interactive configurator,
|
|
||||||
you can only use a custom Openvpn configuration file.
|
|
||||||
[The Wiki's OpenVPN configuration file page](https://github.com/qdm12/gluetun-wiki/blob/main/setup/openvpn-configuration-file.md) describes how to do so.
|
|
||||||
|
|||||||
3
.github/workflows/configs/mlc-config.json
vendored
3
.github/workflows/configs/mlc-config.json
vendored
@@ -8,6 +8,7 @@
|
|||||||
"retryOn429": false,
|
"retryOn429": false,
|
||||||
"fallbackRetryDelay": "30s",
|
"fallbackRetryDelay": "30s",
|
||||||
"aliveStatusCodes": [
|
"aliveStatusCodes": [
|
||||||
200
|
200,
|
||||||
|
429
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
18
Dockerfile
18
Dockerfile
@@ -164,18 +164,20 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
|||||||
# Health
|
# Health
|
||||||
HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \
|
HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \
|
||||||
HEALTH_TARGET_ADDRESS=cloudflare.com:443 \
|
HEALTH_TARGET_ADDRESS=cloudflare.com:443 \
|
||||||
HEALTH_ICMP_TARGET_IP=0.0.0.0 \
|
HEALTH_ICMP_TARGET_IP=1.1.1.1 \
|
||||||
HEALTH_RESTART_VPN=on \
|
HEALTH_RESTART_VPN=on \
|
||||||
# DNS over TLS
|
# DNS
|
||||||
DOT=on \
|
DNS_SERVER=on \
|
||||||
DOT_PROVIDERS=cloudflare \
|
DNS_UPSTREAM_RESOLVER_TYPE=DoT \
|
||||||
DOT_PRIVATE_ADDRESS=127.0.0.1/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1/128,fc00::/7,fe80::/10,::ffff:7f00:1/104,::ffff:a00:0/104,::ffff:a9fe:0/112,::ffff:ac10:0/108,::ffff:c0a8:0/112 \
|
DNS_UPSTREAM_RESOLVERS=cloudflare \
|
||||||
DOT_CACHING=on \
|
DNS_BLOCK_IPS= \
|
||||||
DOT_IPV6=off \
|
DNS_BLOCK_IP_PREFIXES=127.0.0.1/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1/128,fc00::/7,fe80::/10,::ffff:7f00:1/104,::ffff:a00:0/104,::ffff:a9fe:0/112,::ffff:ac10:0/108,::ffff:c0a8:0/112 \
|
||||||
|
DNS_CACHING=on \
|
||||||
|
DNS_UPSTREAM_IPV6=off \
|
||||||
BLOCK_MALICIOUS=on \
|
BLOCK_MALICIOUS=on \
|
||||||
BLOCK_SURVEILLANCE=off \
|
BLOCK_SURVEILLANCE=off \
|
||||||
BLOCK_ADS=off \
|
BLOCK_ADS=off \
|
||||||
UNBLOCK= \
|
DNS_UNBLOCK_HOSTNAMES= \
|
||||||
DNS_UPDATE_PERIOD=24h \
|
DNS_UPDATE_PERIOD=24h \
|
||||||
DNS_ADDRESS=127.0.0.1 \
|
DNS_ADDRESS=127.0.0.1 \
|
||||||
DNS_KEEP_NAMESERVER=off \
|
DNS_KEEP_NAMESERVER=off \
|
||||||
|
|||||||
@@ -581,6 +581,7 @@ type Linker interface {
|
|||||||
LinkDel(link netlink.Link) (err error)
|
LinkDel(link netlink.Link) (err error)
|
||||||
LinkSetUp(link netlink.Link) (linkIndex int, err error)
|
LinkSetUp(link netlink.Link) (linkIndex int, err error)
|
||||||
LinkSetDown(link netlink.Link) (err error)
|
LinkSetDown(link netlink.Link) (err error)
|
||||||
|
LinkSetMTU(link netlink.Link, mtu int) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type clier interface {
|
type clier interface {
|
||||||
|
|||||||
12
go.mod
12
go.mod
@@ -3,20 +3,20 @@ module github.com/qdm12/gluetun
|
|||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/breml/rootcerts v0.3.2
|
github.com/breml/rootcerts v0.3.3
|
||||||
github.com/fatih/color v1.18.0
|
github.com/fatih/color v1.18.0
|
||||||
github.com/golang/mock v1.6.0
|
github.com/golang/mock v1.6.0
|
||||||
github.com/klauspost/compress v1.17.11
|
github.com/klauspost/compress v1.18.1
|
||||||
github.com/klauspost/pgzip v1.2.6
|
github.com/klauspost/pgzip v1.2.6
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3
|
github.com/pelletier/go-toml/v2 v2.2.4
|
||||||
github.com/qdm12/dns/v2 v2.0.0-rc8
|
github.com/qdm12/dns/v2 v2.0.0-rc9
|
||||||
github.com/qdm12/gosettings v0.4.4
|
github.com/qdm12/gosettings v0.4.4
|
||||||
github.com/qdm12/goshutdown v0.3.0
|
github.com/qdm12/goshutdown v0.3.0
|
||||||
github.com/qdm12/gosplash v0.2.0
|
github.com/qdm12/gosplash v0.2.0
|
||||||
github.com/qdm12/gotree v0.3.0
|
github.com/qdm12/gotree v0.3.0
|
||||||
github.com/qdm12/log v0.1.0
|
github.com/qdm12/log v0.1.0
|
||||||
github.com/qdm12/ss-server v0.6.0
|
github.com/qdm12/ss-server v0.6.0
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/ulikunitz/xz v0.5.15
|
github.com/ulikunitz/xz v0.5.15
|
||||||
github.com/vishvananda/netlink v1.3.1
|
github.com/vishvananda/netlink v1.3.1
|
||||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
|
||||||
@@ -47,7 +47,7 @@ require (
|
|||||||
github.com/prometheus/client_model v0.6.1 // indirect
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
github.com/prometheus/common v0.60.1 // indirect
|
github.com/prometheus/common v0.60.1 // indirect
|
||||||
github.com/prometheus/procfs v0.15.1 // indirect
|
github.com/prometheus/procfs v0.15.1 // indirect
|
||||||
github.com/qdm12/goservices v0.1.0 // indirect
|
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 // indirect
|
||||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||||
github.com/vishvananda/netns v0.0.5 // indirect
|
github.com/vishvananda/netns v0.0.5 // indirect
|
||||||
golang.org/x/crypto v0.43.0 // indirect
|
golang.org/x/crypto v0.43.0 // indirect
|
||||||
|
|||||||
24
go.sum
24
go.sum
@@ -1,7 +1,7 @@
|
|||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/breml/rootcerts v0.3.2 h1:gm11iClhK8wFn/GDdINoDaqPkiaGXyVvSwoXINZN+z4=
|
github.com/breml/rootcerts v0.3.3 h1://GnaRtQ/9BY2+GtMk2wtWxVdCRysiaPr5/xBwl7NKw=
|
||||||
github.com/breml/rootcerts v0.3.2/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
|
github.com/breml/rootcerts v0.3.3/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
@@ -16,8 +16,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
|||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
@@ -41,8 +41,8 @@ github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE9
|
|||||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||||
@@ -53,10 +53,10 @@ github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPA
|
|||||||
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
|
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
|
||||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||||
github.com/qdm12/dns/v2 v2.0.0-rc8 h1:kbgKPkbT+79nScfuZ0ZcVhksTGo8IUqQ8TTQGnQlZ18=
|
github.com/qdm12/dns/v2 v2.0.0-rc9 h1:qDzRkHr6993jknNB/ZOCnZOyIG6bsZcl2MIfdeUd0kI=
|
||||||
github.com/qdm12/dns/v2 v2.0.0-rc8/go.mod h1:VaF02KWEL7xNV4oKfG4N9nEv/kR6bqyIcBReCV5NJhw=
|
github.com/qdm12/dns/v2 v2.0.0-rc9/go.mod h1:98foWgXJZ+g8gJIuO+fdO+oWpFei5WShMFTeN4Im2lE=
|
||||||
github.com/qdm12/goservices v0.1.0 h1:9sODefm/yuIGS7ynCkEnNlMTAYn9GzPhtcK4F69JWvc=
|
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 h1:TRGpCU1l0lNwtogEUSs5U+RFceYxkAJUmrGabno7J5c=
|
||||||
github.com/qdm12/goservices v0.1.0/go.mod h1:/JOFsAnHFiSjyoXxa5FlfX903h20K5u/3rLzCjYVMck=
|
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978/go.mod h1:D1Po4CRQLYjccnAR2JsVlN1sBMgQrcNLONbvyuzcdTg=
|
||||||
github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4=
|
github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4=
|
||||||
github.com/qdm12/gosettings v0.4.4/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
|
github.com/qdm12/gosettings v0.4.4/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
|
||||||
github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=
|
github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=
|
||||||
@@ -73,8 +73,8 @@ github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr
|
|||||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
|
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
|
||||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
package settings
|
package settings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/qdm12/dns/v2/pkg/provider"
|
||||||
|
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||||
"github.com/qdm12/gosettings"
|
"github.com/qdm12/gosettings"
|
||||||
"github.com/qdm12/gosettings/reader"
|
"github.com/qdm12/gosettings/reader"
|
||||||
"github.com/qdm12/gotree"
|
"github.com/qdm12/gotree"
|
||||||
@@ -11,10 +15,31 @@ import (
|
|||||||
|
|
||||||
// DNS contains settings to configure DNS.
|
// DNS contains settings to configure DNS.
|
||||||
type DNS struct {
|
type DNS struct {
|
||||||
|
// ServerEnabled is true if the server should be running
|
||||||
|
// and used. It defaults to true, and cannot be nil
|
||||||
|
// in the internal state.
|
||||||
|
ServerEnabled *bool
|
||||||
|
// UpstreamType can be dot or plain, and defaults to dot.
|
||||||
|
UpstreamType string `json:"upstream_type"`
|
||||||
|
// UpdatePeriod is the period to update DNS block lists.
|
||||||
|
// It can be set to 0 to disable the update.
|
||||||
|
// It defaults to 24h and cannot be nil in
|
||||||
|
// the internal state.
|
||||||
|
UpdatePeriod *time.Duration
|
||||||
|
// Providers is a list of DNS providers
|
||||||
|
Providers []string `json:"providers"`
|
||||||
|
// Caching is true if the server should cache
|
||||||
|
// DNS responses.
|
||||||
|
Caching *bool `json:"caching"`
|
||||||
|
// IPv6 is true if the server should connect over IPv6.
|
||||||
|
IPv6 *bool `json:"ipv6"`
|
||||||
|
// Blacklist contains settings to configure the filter
|
||||||
|
// block lists.
|
||||||
|
Blacklist DNSBlacklist
|
||||||
// ServerAddress is the DNS server to use inside
|
// ServerAddress is the DNS server to use inside
|
||||||
// the Go program and for the system.
|
// the Go program and for the system.
|
||||||
// It defaults to '127.0.0.1' to be used with the
|
// It defaults to '127.0.0.1' to be used with the
|
||||||
// DoT server. It cannot be the zero value in the internal
|
// local server. It cannot be the zero value in the internal
|
||||||
// state.
|
// state.
|
||||||
ServerAddress netip.Addr
|
ServerAddress netip.Addr
|
||||||
// KeepNameserver is true if the existing DNS server
|
// KeepNameserver is true if the existing DNS server
|
||||||
@@ -23,20 +48,40 @@ type DNS struct {
|
|||||||
// outside the VPN tunnel since it would go through
|
// outside the VPN tunnel since it would go through
|
||||||
// the local DNS server of your Docker/Kubernetes
|
// the local DNS server of your Docker/Kubernetes
|
||||||
// configuration, which is likely not going through the tunnel.
|
// configuration, which is likely not going through the tunnel.
|
||||||
// This will also disable the DNS over TLS server and the
|
// This will also disable the DNS forwarder server and the
|
||||||
// `ServerAddress` field will be ignored.
|
// `ServerAddress` field will be ignored.
|
||||||
// It defaults to false and cannot be nil in the
|
// It defaults to false and cannot be nil in the
|
||||||
// internal state.
|
// internal state.
|
||||||
KeepNameserver *bool
|
KeepNameserver *bool
|
||||||
// DOT contains settings to configure the DoT
|
|
||||||
// server.
|
|
||||||
DoT DoT
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrDNSUpstreamTypeNotValid = errors.New("DNS upstream type is not valid")
|
||||||
|
ErrDNSUpdatePeriodTooShort = errors.New("update period is too short")
|
||||||
|
)
|
||||||
|
|
||||||
func (d DNS) validate() (err error) {
|
func (d DNS) validate() (err error) {
|
||||||
err = d.DoT.validate()
|
if !helpers.IsOneOf(d.UpstreamType, "dot", "doh", "plain") {
|
||||||
|
return fmt.Errorf("%w: %s", ErrDNSUpstreamTypeNotValid, d.UpstreamType)
|
||||||
|
}
|
||||||
|
|
||||||
|
const minUpdatePeriod = 30 * time.Second
|
||||||
|
if *d.UpdatePeriod != 0 && *d.UpdatePeriod < minUpdatePeriod {
|
||||||
|
return fmt.Errorf("%w: %s must be bigger than %s",
|
||||||
|
ErrDNSUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod)
|
||||||
|
}
|
||||||
|
|
||||||
|
providers := provider.NewProviders()
|
||||||
|
for _, providerName := range d.Providers {
|
||||||
|
_, err := providers.Get(providerName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.Blacklist.validate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("validating DoT settings: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -44,9 +89,15 @@ func (d DNS) validate() (err error) {
|
|||||||
|
|
||||||
func (d *DNS) Copy() (copied DNS) {
|
func (d *DNS) Copy() (copied DNS) {
|
||||||
return DNS{
|
return DNS{
|
||||||
|
ServerEnabled: gosettings.CopyPointer(d.ServerEnabled),
|
||||||
|
UpstreamType: d.UpstreamType,
|
||||||
|
UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod),
|
||||||
|
Providers: gosettings.CopySlice(d.Providers),
|
||||||
|
Caching: gosettings.CopyPointer(d.Caching),
|
||||||
|
IPv6: gosettings.CopyPointer(d.IPv6),
|
||||||
|
Blacklist: d.Blacklist.copy(),
|
||||||
ServerAddress: d.ServerAddress,
|
ServerAddress: d.ServerAddress,
|
||||||
KeepNameserver: gosettings.CopyPointer(d.KeepNameserver),
|
KeepNameserver: gosettings.CopyPointer(d.KeepNameserver),
|
||||||
DoT: d.DoT.copy(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,16 +105,48 @@ func (d *DNS) Copy() (copied DNS) {
|
|||||||
// settings object with any field set in the other
|
// settings object with any field set in the other
|
||||||
// settings.
|
// settings.
|
||||||
func (d *DNS) overrideWith(other DNS) {
|
func (d *DNS) overrideWith(other DNS) {
|
||||||
|
d.ServerEnabled = gosettings.OverrideWithPointer(d.ServerEnabled, other.ServerEnabled)
|
||||||
|
d.UpstreamType = gosettings.OverrideWithComparable(d.UpstreamType, other.UpstreamType)
|
||||||
|
d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod)
|
||||||
|
d.Providers = gosettings.OverrideWithSlice(d.Providers, other.Providers)
|
||||||
|
d.Caching = gosettings.OverrideWithPointer(d.Caching, other.Caching)
|
||||||
|
d.IPv6 = gosettings.OverrideWithPointer(d.IPv6, other.IPv6)
|
||||||
|
d.Blacklist.overrideWith(other.Blacklist)
|
||||||
d.ServerAddress = gosettings.OverrideWithValidator(d.ServerAddress, other.ServerAddress)
|
d.ServerAddress = gosettings.OverrideWithValidator(d.ServerAddress, other.ServerAddress)
|
||||||
d.KeepNameserver = gosettings.OverrideWithPointer(d.KeepNameserver, other.KeepNameserver)
|
d.KeepNameserver = gosettings.OverrideWithPointer(d.KeepNameserver, other.KeepNameserver)
|
||||||
d.DoT.overrideWith(other.DoT)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNS) setDefaults() {
|
func (d *DNS) setDefaults() {
|
||||||
localhost := netip.AddrFrom4([4]byte{127, 0, 0, 1})
|
d.ServerEnabled = gosettings.DefaultPointer(d.ServerEnabled, true)
|
||||||
d.ServerAddress = gosettings.DefaultValidator(d.ServerAddress, localhost)
|
d.UpstreamType = gosettings.DefaultComparable(d.UpstreamType, "dot")
|
||||||
|
const defaultUpdatePeriod = 24 * time.Hour
|
||||||
|
d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod)
|
||||||
|
d.Providers = gosettings.DefaultSlice(d.Providers, []string{
|
||||||
|
provider.Cloudflare().Name,
|
||||||
|
})
|
||||||
|
d.Caching = gosettings.DefaultPointer(d.Caching, true)
|
||||||
|
d.IPv6 = gosettings.DefaultPointer(d.IPv6, false)
|
||||||
|
d.Blacklist.setDefaults()
|
||||||
|
d.ServerAddress = gosettings.DefaultValidator(d.ServerAddress,
|
||||||
|
netip.AddrFrom4([4]byte{127, 0, 0, 1}))
|
||||||
d.KeepNameserver = gosettings.DefaultPointer(d.KeepNameserver, false)
|
d.KeepNameserver = gosettings.DefaultPointer(d.KeepNameserver, false)
|
||||||
d.DoT.setDefaults()
|
}
|
||||||
|
|
||||||
|
func (d DNS) GetFirstPlaintextIPv4() (ipv4 netip.Addr) {
|
||||||
|
localhost := netip.AddrFrom4([4]byte{127, 0, 0, 1})
|
||||||
|
if d.ServerAddress.Compare(localhost) != 0 && d.ServerAddress.Is4() {
|
||||||
|
return d.ServerAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
providers := provider.NewProviders()
|
||||||
|
provider, err := providers.Get(d.Providers[0])
|
||||||
|
if err != nil {
|
||||||
|
// Settings should be validated before calling this function,
|
||||||
|
// so an error happening here is a programming error.
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider.Plain.IPv4[0].Addr()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d DNS) String() string {
|
func (d DNS) String() string {
|
||||||
@@ -77,11 +160,63 @@ func (d DNS) toLinesNode() (node *gotree.Node) {
|
|||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
node.Appendf("DNS server address to use: %s", d.ServerAddress)
|
node.Appendf("DNS server address to use: %s", d.ServerAddress)
|
||||||
node.AppendNode(d.DoT.toLinesNode())
|
|
||||||
|
node.Appendf("DNS forwarder server enabled: %s", gosettings.BoolToYesNo(d.ServerEnabled))
|
||||||
|
if !*d.ServerEnabled {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
node.Appendf("Upstream resolver type: %s", d.UpstreamType)
|
||||||
|
|
||||||
|
upstreamResolvers := node.Append("Upstream resolvers:")
|
||||||
|
for _, provider := range d.Providers {
|
||||||
|
upstreamResolvers.Append(provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
node.Appendf("Caching: %s", gosettings.BoolToYesNo(d.Caching))
|
||||||
|
node.Appendf("IPv6: %s", gosettings.BoolToYesNo(d.IPv6))
|
||||||
|
|
||||||
|
update := "disabled"
|
||||||
|
if *d.UpdatePeriod > 0 {
|
||||||
|
update = "every " + d.UpdatePeriod.String()
|
||||||
|
}
|
||||||
|
node.Appendf("Update period: %s", update)
|
||||||
|
|
||||||
|
node.AppendNode(d.Blacklist.toLinesNode())
|
||||||
|
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNS) read(r *reader.Reader) (err error) {
|
func (d *DNS) read(r *reader.Reader) (err error) {
|
||||||
|
d.ServerEnabled, err = r.BoolPtr("DNS_SERVER", reader.RetroKeys("DOT"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.UpstreamType = r.String("DNS_UPSTREAM_RESOLVER_TYPE")
|
||||||
|
|
||||||
|
d.UpdatePeriod, err = r.DurationPtr("DNS_UPDATE_PERIOD")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Providers = r.CSV("DNS_UPSTREAM_RESOLVERS", reader.RetroKeys("DOT_PROVIDERS"))
|
||||||
|
|
||||||
|
d.Caching, err = r.BoolPtr("DNS_CACHING", reader.RetroKeys("DOT_CACHING"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.IPv6, err = r.BoolPtr("DNS_UPSTREAM_IPV6", reader.RetroKeys("DOT_IPV6"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.Blacklist.read(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
d.ServerAddress, err = r.NetipAddr("DNS_ADDRESS", reader.RetroKeys("DNS_PLAINTEXT_ADDRESS"))
|
d.ServerAddress, err = r.NetipAddr("DNS_ADDRESS", reader.RetroKeys("DNS_PLAINTEXT_ADDRESS"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -92,10 +227,5 @@ func (d *DNS) read(r *reader.Reader) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = d.DoT.read(r)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("DNS over TLS settings: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,23 +149,45 @@ func (b *DNSBlacklist) read(r *reader.Reader) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
b.AddBlockedIPs, b.AddBlockedIPPrefixes,
|
b.AddBlockedIPs, b.AddBlockedIPPrefixes, err = readDNSBlockedIPs(r)
|
||||||
err = readDoTPrivateAddresses(r) // TODO v4 split in 2
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
b.AllowedHosts = r.CSV("UNBLOCK") // TODO v4 change name
|
b.AllowedHosts = r.CSV("DNS_UNBLOCK_HOSTNAMES", reader.RetroKeys("UNBLOCK"))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrPrivateAddressNotValid = errors.New("private address is not a valid IP or CIDR range")
|
func readDNSBlockedIPs(r *reader.Reader) (ips []netip.Addr,
|
||||||
|
|
||||||
func readDoTPrivateAddresses(reader *reader.Reader) (ips []netip.Addr,
|
|
||||||
ipPrefixes []netip.Prefix, err error,
|
ipPrefixes []netip.Prefix, err error,
|
||||||
) {
|
) {
|
||||||
privateAddresses := reader.CSV("DOT_PRIVATE_ADDRESS")
|
ips, err = r.CSVNetipAddresses("DNS_BLOCK_IPS")
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
ipPrefixes, err = r.CSVNetipPrefixes("DNS_BLOCK_IP_PREFIXES")
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO v4 remove this block below
|
||||||
|
privateIPs, privateIPPrefixes, err := readDNSPrivateAddresses(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
ips = append(ips, privateIPs...)
|
||||||
|
ipPrefixes = append(ipPrefixes, privateIPPrefixes...)
|
||||||
|
|
||||||
|
return ips, ipPrefixes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrPrivateAddressNotValid = errors.New("private address is not a valid IP or CIDR range")
|
||||||
|
|
||||||
|
func readDNSPrivateAddresses(r *reader.Reader) (ips []netip.Addr,
|
||||||
|
ipPrefixes []netip.Prefix, err error,
|
||||||
|
) {
|
||||||
|
privateAddresses := r.CSV("DOT_PRIVATE_ADDRESS", reader.IsRetro("DNS_BLOCK_IP_PREFIXES"))
|
||||||
if len(privateAddresses) == 0 {
|
if len(privateAddresses) == 0 {
|
||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,170 +0,0 @@
|
|||||||
package settings
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/netip"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/qdm12/dns/v2/pkg/provider"
|
|
||||||
"github.com/qdm12/gosettings"
|
|
||||||
"github.com/qdm12/gosettings/reader"
|
|
||||||
"github.com/qdm12/gotree"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DoT contains settings to configure the DoT server.
|
|
||||||
type DoT struct {
|
|
||||||
// Enabled is true if the DoT server should be running
|
|
||||||
// and used. It defaults to true, and cannot be nil
|
|
||||||
// in the internal state.
|
|
||||||
Enabled *bool
|
|
||||||
// UpdatePeriod is the period to update DNS block lists.
|
|
||||||
// It can be set to 0 to disable the update.
|
|
||||||
// It defaults to 24h and cannot be nil in
|
|
||||||
// the internal state.
|
|
||||||
UpdatePeriod *time.Duration
|
|
||||||
// Providers is a list of DNS over TLS providers
|
|
||||||
Providers []string `json:"providers"`
|
|
||||||
// Caching is true if the DoT server should cache
|
|
||||||
// DNS responses.
|
|
||||||
Caching *bool `json:"caching"`
|
|
||||||
// IPv6 is true if the DoT server should connect over IPv6.
|
|
||||||
IPv6 *bool `json:"ipv6"`
|
|
||||||
// Blacklist contains settings to configure the filter
|
|
||||||
// block lists.
|
|
||||||
Blacklist DNSBlacklist
|
|
||||||
}
|
|
||||||
|
|
||||||
var ErrDoTUpdatePeriodTooShort = errors.New("update period is too short")
|
|
||||||
|
|
||||||
func (d DoT) validate() (err error) {
|
|
||||||
const minUpdatePeriod = 30 * time.Second
|
|
||||||
if *d.UpdatePeriod != 0 && *d.UpdatePeriod < minUpdatePeriod {
|
|
||||||
return fmt.Errorf("%w: %s must be bigger than %s",
|
|
||||||
ErrDoTUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod)
|
|
||||||
}
|
|
||||||
|
|
||||||
providers := provider.NewProviders()
|
|
||||||
for _, providerName := range d.Providers {
|
|
||||||
_, err := providers.Get(providerName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = d.Blacklist.validate()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DoT) copy() (copied DoT) {
|
|
||||||
return DoT{
|
|
||||||
Enabled: gosettings.CopyPointer(d.Enabled),
|
|
||||||
UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod),
|
|
||||||
Providers: gosettings.CopySlice(d.Providers),
|
|
||||||
Caching: gosettings.CopyPointer(d.Caching),
|
|
||||||
IPv6: gosettings.CopyPointer(d.IPv6),
|
|
||||||
Blacklist: d.Blacklist.copy(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// overrideWith overrides fields of the receiver
|
|
||||||
// settings object with any field set in the other
|
|
||||||
// settings.
|
|
||||||
func (d *DoT) overrideWith(other DoT) {
|
|
||||||
d.Enabled = gosettings.OverrideWithPointer(d.Enabled, other.Enabled)
|
|
||||||
d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod)
|
|
||||||
d.Providers = gosettings.OverrideWithSlice(d.Providers, other.Providers)
|
|
||||||
d.Caching = gosettings.OverrideWithPointer(d.Caching, other.Caching)
|
|
||||||
d.IPv6 = gosettings.OverrideWithPointer(d.IPv6, other.IPv6)
|
|
||||||
d.Blacklist.overrideWith(other.Blacklist)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DoT) setDefaults() {
|
|
||||||
d.Enabled = gosettings.DefaultPointer(d.Enabled, true)
|
|
||||||
const defaultUpdatePeriod = 24 * time.Hour
|
|
||||||
d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod)
|
|
||||||
d.Providers = gosettings.DefaultSlice(d.Providers, []string{
|
|
||||||
provider.Cloudflare().Name,
|
|
||||||
})
|
|
||||||
d.Caching = gosettings.DefaultPointer(d.Caching, true)
|
|
||||||
d.IPv6 = gosettings.DefaultPointer(d.IPv6, false)
|
|
||||||
d.Blacklist.setDefaults()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d DoT) GetFirstPlaintextIPv4() (ipv4 netip.Addr) {
|
|
||||||
providers := provider.NewProviders()
|
|
||||||
provider, err := providers.Get(d.Providers[0])
|
|
||||||
if err != nil {
|
|
||||||
// Settings should be validated before calling this function,
|
|
||||||
// so an error happening here is a programming error.
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return provider.DoT.IPv4[0].Addr()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d DoT) String() string {
|
|
||||||
return d.toLinesNode().String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d DoT) toLinesNode() (node *gotree.Node) {
|
|
||||||
node = gotree.New("DNS over TLS settings:")
|
|
||||||
|
|
||||||
node.Appendf("Enabled: %s", gosettings.BoolToYesNo(d.Enabled))
|
|
||||||
if !*d.Enabled {
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
|
|
||||||
update := "disabled"
|
|
||||||
if *d.UpdatePeriod > 0 {
|
|
||||||
update = "every " + d.UpdatePeriod.String()
|
|
||||||
}
|
|
||||||
node.Appendf("Update period: %s", update)
|
|
||||||
|
|
||||||
upstreamResolvers := node.Append("Upstream resolvers:")
|
|
||||||
for _, provider := range d.Providers {
|
|
||||||
upstreamResolvers.Append(provider)
|
|
||||||
}
|
|
||||||
|
|
||||||
node.Appendf("Caching: %s", gosettings.BoolToYesNo(d.Caching))
|
|
||||||
node.Appendf("IPv6: %s", gosettings.BoolToYesNo(d.IPv6))
|
|
||||||
|
|
||||||
node.AppendNode(d.Blacklist.toLinesNode())
|
|
||||||
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DoT) read(reader *reader.Reader) (err error) {
|
|
||||||
d.Enabled, err = reader.BoolPtr("DOT")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
d.UpdatePeriod, err = reader.DurationPtr("DNS_UPDATE_PERIOD")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
d.Providers = reader.CSV("DOT_PROVIDERS")
|
|
||||||
|
|
||||||
d.Caching, err = reader.BoolPtr("DOT_CACHING")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
d.IPv6, err = reader.BoolPtr("DOT_IPV6")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = d.Blacklist.read(reader)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -29,7 +29,7 @@ type Health struct {
|
|||||||
// It cannot be the empty string in the internal state.
|
// It cannot be the empty string in the internal state.
|
||||||
TargetAddress string
|
TargetAddress string
|
||||||
// ICMPTargetIP is the IP address to use for ICMP echo requests
|
// ICMPTargetIP is the IP address to use for ICMP echo requests
|
||||||
// in the health checker. It can be set to an unspecified address
|
// in the health checker. It can be set to an unspecified address (0.0.0.0)
|
||||||
// such that the VPN server IP is used, which is also the default behavior.
|
// such that the VPN server IP is used, which is also the default behavior.
|
||||||
ICMPTargetIP netip.Addr
|
ICMPTargetIP netip.Addr
|
||||||
// RestartVPN indicates whether to restart the VPN connection
|
// RestartVPN indicates whether to restart the VPN connection
|
||||||
|
|||||||
@@ -177,10 +177,10 @@ func (s Settings) Warnings() (warnings []string) {
|
|||||||
// TODO remove in v4
|
// TODO remove in v4
|
||||||
if s.DNS.ServerAddress.Unmap().Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) != 0 {
|
if s.DNS.ServerAddress.Unmap().Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) != 0 {
|
||||||
warnings = append(warnings, "DNS address is set to "+s.DNS.ServerAddress.String()+
|
warnings = append(warnings, "DNS address is set to "+s.DNS.ServerAddress.String()+
|
||||||
" so the DNS over TLS (DoT) server will not be used."+
|
" so the local forwarding DNS server will not be used."+
|
||||||
" The default value changed to 127.0.0.1 so it uses the internal DoT serves."+
|
" The default value changed to 127.0.0.1 so it uses the internal DNS server."+
|
||||||
" If the DoT server fails to start, the IPv4 address of the first plaintext DNS server"+
|
" If this server fails to start, the IPv4 address of the first plaintext DNS server"+
|
||||||
" corresponding to the first DoT provider chosen is used.")
|
" corresponding to the first DNS provider chosen is used.")
|
||||||
}
|
}
|
||||||
|
|
||||||
return warnings
|
return warnings
|
||||||
|
|||||||
@@ -40,17 +40,17 @@ func Test_Settings_String(t *testing.T) {
|
|||||||
├── DNS settings:
|
├── DNS settings:
|
||||||
| ├── Keep existing nameserver(s): no
|
| ├── Keep existing nameserver(s): no
|
||||||
| ├── DNS server address to use: 127.0.0.1
|
| ├── DNS server address to use: 127.0.0.1
|
||||||
| └── DNS over TLS settings:
|
| ├── DNS forwarder server enabled: yes
|
||||||
| ├── Enabled: yes
|
| ├── Upstream resolver type: dot
|
||||||
| ├── Update period: every 24h0m0s
|
| ├── Upstream resolvers:
|
||||||
| ├── Upstream resolvers:
|
| | └── Cloudflare
|
||||||
| | └── Cloudflare
|
| ├── Caching: yes
|
||||||
| ├── Caching: yes
|
| ├── IPv6: no
|
||||||
| ├── IPv6: no
|
| ├── Update period: every 24h0m0s
|
||||||
| └── DNS filtering settings:
|
| └── DNS filtering settings:
|
||||||
| ├── Block malicious: yes
|
| ├── Block malicious: yes
|
||||||
| ├── Block ads: no
|
| ├── Block ads: no
|
||||||
| └── Block surveillance: yes
|
| └── Block surveillance: yes
|
||||||
├── Firewall settings:
|
├── Firewall settings:
|
||||||
| └── Enabled: yes
|
| └── Enabled: yes
|
||||||
├── Log settings:
|
├── Log settings:
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ type Wireguard struct {
|
|||||||
// It has been lowered to 1320 following quite a bit of
|
// It has been lowered to 1320 following quite a bit of
|
||||||
// investigation in the issue:
|
// investigation in the issue:
|
||||||
// https://github.com/qdm12/gluetun/issues/2533.
|
// https://github.com/qdm12/gluetun/issues/2533.
|
||||||
|
// Note this should now be replaced with the PMTUD feature.
|
||||||
MTU uint16 `json:"mtu"`
|
MTU uint16 `json:"mtu"`
|
||||||
// Implementation is the Wireguard implementation to use.
|
// Implementation is the Wireguard implementation to use.
|
||||||
// It can be "auto", "userspace" or "kernelspace".
|
// It can be "auto", "userspace" or "kernelspace".
|
||||||
|
|||||||
@@ -10,15 +10,7 @@ import (
|
|||||||
func (l *Loop) useUnencryptedDNS(fallback bool) {
|
func (l *Loop) useUnencryptedDNS(fallback bool) {
|
||||||
settings := l.GetSettings()
|
settings := l.GetSettings()
|
||||||
|
|
||||||
// Try with user provided plaintext ip address
|
targetIP := settings.GetFirstPlaintextIPv4()
|
||||||
// if it's not 127.0.0.1 (default for DoT), otherwise
|
|
||||||
// use the first DoT provider ipv4 address found.
|
|
||||||
var targetIP netip.Addr
|
|
||||||
if settings.ServerAddress.Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) != 0 {
|
|
||||||
targetIP = settings.ServerAddress
|
|
||||||
} else {
|
|
||||||
targetIP = settings.DoT.GetFirstPlaintextIPv4()
|
|
||||||
}
|
|
||||||
|
|
||||||
if fallback {
|
if fallback {
|
||||||
l.logger.Info("falling back on plaintext DNS at address " + targetIP.String())
|
l.logger.Info("falling back on plaintext DNS at address " + targetIP.String())
|
||||||
@@ -27,14 +19,15 @@ func (l *Loop) useUnencryptedDNS(fallback bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const dialTimeout = 3 * time.Second
|
const dialTimeout = 3 * time.Second
|
||||||
|
const defaultDNSPort = 53
|
||||||
settingsInternalDNS := nameserver.SettingsInternalDNS{
|
settingsInternalDNS := nameserver.SettingsInternalDNS{
|
||||||
IP: targetIP,
|
AddrPort: netip.AddrPortFrom(targetIP, defaultDNSPort),
|
||||||
Timeout: dialTimeout,
|
Timeout: dialTimeout,
|
||||||
}
|
}
|
||||||
nameserver.UseDNSInternally(settingsInternalDNS)
|
nameserver.UseDNSInternally(settingsInternalDNS)
|
||||||
|
|
||||||
settingsSystemWide := nameserver.SettingsSystemDNS{
|
settingsSystemWide := nameserver.SettingsSystemDNS{
|
||||||
IP: targetIP,
|
IPs: []netip.Addr{targetIP},
|
||||||
ResolvPath: l.resolvConf,
|
ResolvPath: l.resolvConf,
|
||||||
}
|
}
|
||||||
err := nameserver.UseDNSSystemWide(settingsSystemWide)
|
err := nameserver.UseDNSSystemWide(settingsSystemWide)
|
||||||
|
|||||||
@@ -26,12 +26,12 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for ctx.Err() == nil {
|
for ctx.Err() == nil {
|
||||||
// Upper scope variables for the DNS over TLS server only
|
// Upper scope variables for the DNS forwarder server only
|
||||||
// Their values are to be used if DOT=off
|
// Their values are to be used if DOT=off
|
||||||
var runError <-chan error
|
var runError <-chan error
|
||||||
|
|
||||||
settings := l.GetSettings()
|
settings := l.GetSettings()
|
||||||
for !*settings.KeepNameserver && *settings.DoT.Enabled {
|
for !*settings.KeepNameserver && *settings.ServerEnabled {
|
||||||
var err error
|
var err error
|
||||||
runError, err = l.setupServer(ctx)
|
runError, err = l.setupServer(ctx)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -56,7 +56,7 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
settings = l.GetSettings()
|
settings = l.GetSettings()
|
||||||
if !*settings.KeepNameserver && !*settings.DoT.Enabled {
|
if !*settings.KeepNameserver && !*settings.ServerEnabled {
|
||||||
const fallback = false
|
const fallback = false
|
||||||
l.useUnencryptedDNS(fallback)
|
l.useUnencryptedDNS(fallback)
|
||||||
}
|
}
|
||||||
@@ -101,6 +101,6 @@ func (l *Loop) runWait(ctx context.Context, runError <-chan error) (exitLoop boo
|
|||||||
func (l *Loop) stopServer() {
|
func (l *Loop) stopServer() {
|
||||||
stopErr := l.server.Stop()
|
stopErr := l.server.Stop()
|
||||||
if stopErr != nil {
|
if stopErr != nil {
|
||||||
l.logger.Error("stopping DoT server: " + stopErr.Error())
|
l.logger.Error("stopping server: " + stopErr.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/qdm12/dns/v2/pkg/doh"
|
||||||
"github.com/qdm12/dns/v2/pkg/dot"
|
"github.com/qdm12/dns/v2/pkg/dot"
|
||||||
cachemiddleware "github.com/qdm12/dns/v2/pkg/middlewares/cache"
|
cachemiddleware "github.com/qdm12/dns/v2/pkg/middlewares/cache"
|
||||||
"github.com/qdm12/dns/v2/pkg/middlewares/cache/lru"
|
"github.com/qdm12/dns/v2/pkg/middlewares/cache/lru"
|
||||||
filtermiddleware "github.com/qdm12/dns/v2/pkg/middlewares/filter"
|
filtermiddleware "github.com/qdm12/dns/v2/pkg/middlewares/filter"
|
||||||
"github.com/qdm12/dns/v2/pkg/middlewares/filter/mapfilter"
|
"github.com/qdm12/dns/v2/pkg/middlewares/filter/mapfilter"
|
||||||
|
"github.com/qdm12/dns/v2/pkg/plain"
|
||||||
"github.com/qdm12/dns/v2/pkg/provider"
|
"github.com/qdm12/dns/v2/pkg/provider"
|
||||||
"github.com/qdm12/dns/v2/pkg/server"
|
"github.com/qdm12/dns/v2/pkg/server"
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
@@ -22,33 +24,62 @@ func (l *Loop) SetSettings(ctx context.Context, settings settings.DNS) (
|
|||||||
return l.state.SetSettings(ctx, settings)
|
return l.state.SetSettings(ctx, settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildDoTSettings(settings settings.DNS,
|
func buildServerSettings(settings settings.DNS,
|
||||||
filter *mapfilter.Filter, logger Logger) (
|
filter *mapfilter.Filter, logger Logger) (
|
||||||
serverSettings server.Settings, err error,
|
serverSettings server.Settings, err error,
|
||||||
) {
|
) {
|
||||||
serverSettings.Logger = logger
|
serverSettings.Logger = logger
|
||||||
|
|
||||||
var dotSettings dot.Settings
|
|
||||||
providersData := provider.NewProviders()
|
providersData := provider.NewProviders()
|
||||||
dotSettings.UpstreamResolvers = make([]provider.Provider, len(settings.DoT.Providers))
|
upstreamResolvers := make([]provider.Provider, len(settings.Providers))
|
||||||
for i := range settings.DoT.Providers {
|
for i := range settings.Providers {
|
||||||
var err error
|
var err error
|
||||||
dotSettings.UpstreamResolvers[i], err = providersData.Get(settings.DoT.Providers[i])
|
upstreamResolvers[i], err = providersData.Get(settings.Providers[i])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err) // this should already had been checked
|
panic(err) // this should already had been checked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dotSettings.IPVersion = "ipv4"
|
|
||||||
if *settings.DoT.IPv6 {
|
ipVersion := "ipv4"
|
||||||
dotSettings.IPVersion = "ipv6"
|
if *settings.IPv6 {
|
||||||
|
ipVersion = "ipv6"
|
||||||
}
|
}
|
||||||
|
|
||||||
serverSettings.Dialer, err = dot.New(dotSettings)
|
var dialer server.Dialer
|
||||||
if err != nil {
|
switch settings.UpstreamType {
|
||||||
return server.Settings{}, fmt.Errorf("creating DNS over TLS dialer: %w", err)
|
case "dot":
|
||||||
|
dialerSettings := dot.Settings{
|
||||||
|
UpstreamResolvers: upstreamResolvers,
|
||||||
|
IPVersion: ipVersion,
|
||||||
|
}
|
||||||
|
dialer, err = dot.New(dialerSettings)
|
||||||
|
if err != nil {
|
||||||
|
return server.Settings{}, fmt.Errorf("creating DNS over TLS dialer: %w", err)
|
||||||
|
}
|
||||||
|
case "doh":
|
||||||
|
dialerSettings := doh.Settings{
|
||||||
|
UpstreamResolvers: upstreamResolvers,
|
||||||
|
IPVersion: ipVersion,
|
||||||
|
}
|
||||||
|
dialer, err = doh.New(dialerSettings)
|
||||||
|
if err != nil {
|
||||||
|
return server.Settings{}, fmt.Errorf("creating DNS over HTTPS dialer: %w", err)
|
||||||
|
}
|
||||||
|
case "plain":
|
||||||
|
dialerSettings := plain.Settings{
|
||||||
|
UpstreamResolvers: upstreamResolvers,
|
||||||
|
IPVersion: ipVersion,
|
||||||
|
}
|
||||||
|
dialer, err = plain.New(dialerSettings)
|
||||||
|
if err != nil {
|
||||||
|
return server.Settings{}, fmt.Errorf("creating plain DNS dialer: %w", err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic("unknown upstream type: " + settings.UpstreamType)
|
||||||
}
|
}
|
||||||
|
serverSettings.Dialer = dialer
|
||||||
|
|
||||||
if *settings.DoT.Caching {
|
if *settings.Caching {
|
||||||
lruCache, err := lru.New(lru.Settings{})
|
lruCache, err := lru.New(lru.Settings{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return server.Settings{}, fmt.Errorf("creating LRU cache: %w", err)
|
return server.Settings{}, fmt.Errorf("creating LRU cache: %w", err)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
"github.com/qdm12/dns/v2/pkg/check"
|
"github.com/qdm12/dns/v2/pkg/check"
|
||||||
"github.com/qdm12/dns/v2/pkg/nameserver"
|
"github.com/qdm12/dns/v2/pkg/nameserver"
|
||||||
@@ -20,14 +21,14 @@ func (l *Loop) setupServer(ctx context.Context) (runError <-chan error, err erro
|
|||||||
|
|
||||||
settings := l.GetSettings()
|
settings := l.GetSettings()
|
||||||
|
|
||||||
dotSettings, err := buildDoTSettings(settings, l.filter, l.logger)
|
serverSettings, err := buildServerSettings(settings, l.filter, l.logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("building DoT settings: %w", err)
|
return nil, fmt.Errorf("building server settings: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
server, err := server.New(dotSettings)
|
server, err := server.New(serverSettings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("creating DoT server: %w", err)
|
return nil, fmt.Errorf("creating server: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
runError, err = server.Start(ctx)
|
runError, err = server.Start(ctx)
|
||||||
@@ -37,11 +38,12 @@ func (l *Loop) setupServer(ctx context.Context) (runError <-chan error, err erro
|
|||||||
l.server = server
|
l.server = server
|
||||||
|
|
||||||
// use internal DNS server
|
// use internal DNS server
|
||||||
|
const defaultDNSPort = 53
|
||||||
nameserver.UseDNSInternally(nameserver.SettingsInternalDNS{
|
nameserver.UseDNSInternally(nameserver.SettingsInternalDNS{
|
||||||
IP: settings.ServerAddress,
|
AddrPort: netip.AddrPortFrom(settings.ServerAddress, defaultDNSPort),
|
||||||
})
|
})
|
||||||
err = nameserver.UseDNSSystemWide(nameserver.SettingsSystemDNS{
|
err = nameserver.UseDNSSystemWide(nameserver.SettingsSystemDNS{
|
||||||
IP: settings.ServerAddress,
|
IPs: []netip.Addr{settings.ServerAddress},
|
||||||
ResolvPath: l.resolvConf,
|
ResolvPath: l.resolvConf,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func (s *State) SetSettings(ctx context.Context, settings settings.DNS) (
|
|||||||
|
|
||||||
// Check for only update period change
|
// Check for only update period change
|
||||||
tempSettings := s.settings.Copy()
|
tempSettings := s.settings.Copy()
|
||||||
*tempSettings.DoT.UpdatePeriod = *settings.DoT.UpdatePeriod
|
*tempSettings.UpdatePeriod = *settings.UpdatePeriod
|
||||||
onlyUpdatePeriodChanged := reflect.DeepEqual(tempSettings, settings)
|
onlyUpdatePeriodChanged := reflect.DeepEqual(tempSettings, settings)
|
||||||
|
|
||||||
s.settings = settings
|
s.settings = settings
|
||||||
@@ -40,7 +40,7 @@ func (s *State) SetSettings(ctx context.Context, settings settings.DNS) (
|
|||||||
|
|
||||||
// Restart
|
// Restart
|
||||||
_, _ = s.statusApplier.ApplyStatus(ctx, constants.Stopped)
|
_, _ = s.statusApplier.ApplyStatus(ctx, constants.Stopped)
|
||||||
if *settings.DoT.Enabled {
|
if *settings.ServerEnabled {
|
||||||
outcome, _ = s.statusApplier.ApplyStatus(ctx, constants.Running)
|
outcome, _ = s.statusApplier.ApplyStatus(ctx, constants.Running)
|
||||||
}
|
}
|
||||||
return outcome
|
return outcome
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ func (l *Loop) RunRestartTicker(ctx context.Context, done chan<- struct{}) {
|
|||||||
timer.Stop()
|
timer.Stop()
|
||||||
timerIsStopped := true
|
timerIsStopped := true
|
||||||
settings := l.GetSettings()
|
settings := l.GetSettings()
|
||||||
if period := *settings.DoT.UpdatePeriod; period > 0 {
|
if period := *settings.UpdatePeriod; period > 0 {
|
||||||
timer.Reset(period)
|
timer.Reset(period)
|
||||||
timerIsStopped = false
|
timerIsStopped = false
|
||||||
}
|
}
|
||||||
@@ -43,14 +43,14 @@ func (l *Loop) RunRestartTicker(ctx context.Context, done chan<- struct{}) {
|
|||||||
_, _ = l.statusManager.ApplyStatus(ctx, constants.Running)
|
_, _ = l.statusManager.ApplyStatus(ctx, constants.Running)
|
||||||
|
|
||||||
settings := l.GetSettings()
|
settings := l.GetSettings()
|
||||||
timer.Reset(*settings.DoT.UpdatePeriod)
|
timer.Reset(*settings.UpdatePeriod)
|
||||||
case <-l.updateTicker:
|
case <-l.updateTicker:
|
||||||
if !timer.Stop() {
|
if !timer.Stop() {
|
||||||
<-timer.C
|
<-timer.C
|
||||||
}
|
}
|
||||||
timerIsStopped = true
|
timerIsStopped = true
|
||||||
settings := l.GetSettings()
|
settings := l.GetSettings()
|
||||||
newUpdatePeriod := *settings.DoT.UpdatePeriod
|
newUpdatePeriod := *settings.UpdatePeriod
|
||||||
if newUpdatePeriod == 0 {
|
if newUpdatePeriod == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ func (l *Loop) updateFiles(ctx context.Context) (err error) {
|
|||||||
settings := l.GetSettings()
|
settings := l.GetSettings()
|
||||||
|
|
||||||
l.logger.Info("downloading hostnames and IP block lists")
|
l.logger.Info("downloading hostnames and IP block lists")
|
||||||
blacklistSettings := settings.DoT.Blacklist.ToBlockBuilderSettings(l.client)
|
blacklistSettings := settings.Blacklist.ToBlockBuilderSettings(l.client)
|
||||||
|
|
||||||
blockBuilder, err := blockbuilder.New(blacklistSettings)
|
blockBuilder, err := blockbuilder.New(blacklistSettings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -323,12 +323,12 @@ var ErrProtocolUnknown = errors.New("unknown protocol")
|
|||||||
|
|
||||||
func parseProtocol(s string) (protocol string, err error) {
|
func parseProtocol(s string) (protocol string, err error) {
|
||||||
switch s {
|
switch s {
|
||||||
case "0":
|
case "0", "all":
|
||||||
case "1":
|
case "1", "icmp":
|
||||||
protocol = "icmp"
|
protocol = "icmp"
|
||||||
case "6":
|
case "6", "tcp":
|
||||||
protocol = "tcp"
|
protocol = "tcp"
|
||||||
case "17":
|
case "17", "udp":
|
||||||
protocol = "udp"
|
protocol = "udp"
|
||||||
default:
|
default:
|
||||||
return "", fmt.Errorf("%w: %s", ErrProtocolUnknown, s)
|
return "", fmt.Errorf("%w: %s", ErrProtocolUnknown, s)
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ num pkts bytes target prot opt in out source destinati
|
|||||||
2 0 0 ACCEPT 6 -- tun0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:55405
|
2 0 0 ACCEPT 6 -- tun0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:55405
|
||||||
3 0 0 ACCEPT 1 -- tun0 * 0.0.0.0/0 0.0.0.0/0
|
3 0 0 ACCEPT 1 -- tun0 * 0.0.0.0/0 0.0.0.0/0
|
||||||
4 0 0 DROP 0 -- tun0 * 1.2.3.4 0.0.0.0/0
|
4 0 0 DROP 0 -- tun0 * 1.2.3.4 0.0.0.0/0
|
||||||
|
5 0 0 ACCEPT all -- tun0 * 1.2.3.4 0.0.0.0/0
|
||||||
`,
|
`,
|
||||||
table: chain{
|
table: chain{
|
||||||
name: "INPUT",
|
name: "INPUT",
|
||||||
@@ -111,6 +112,17 @@ num pkts bytes target prot opt in out source destinati
|
|||||||
source: netip.MustParsePrefix("1.2.3.4/32"),
|
source: netip.MustParsePrefix("1.2.3.4/32"),
|
||||||
destination: netip.MustParsePrefix("0.0.0.0/0"),
|
destination: netip.MustParsePrefix("0.0.0.0/0"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
lineNumber: 5,
|
||||||
|
packets: 0,
|
||||||
|
bytes: 0,
|
||||||
|
target: "ACCEPT",
|
||||||
|
protocol: "",
|
||||||
|
inputInterface: "tun0",
|
||||||
|
outputInterface: "*",
|
||||||
|
source: netip.MustParsePrefix("1.2.3.4/32"),
|
||||||
|
destination: netip.MustParsePrefix("0.0.0.0/0"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,33 +5,60 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/qdm12/dns/v2/pkg/provider"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client is a simple plaintext UDP DNS client, to be used for healthchecks.
|
// Client is a simple plaintext UDP DNS client, to be used for healthchecks.
|
||||||
// Note the client connects to a DNS server only over UDP on port 53,
|
// Note the client connects to a DNS server only over UDP on port 53,
|
||||||
// because we don't want to use DoT or DoH and impact the TCP connections
|
// because we don't want to use DoT or DoH and impact the TCP connections
|
||||||
// when running a healthcheck.
|
// when running a healthcheck.
|
||||||
type Client struct{}
|
type Client struct {
|
||||||
|
serverAddrs []netip.AddrPort
|
||||||
|
dnsIPIndex int
|
||||||
|
}
|
||||||
|
|
||||||
func New() *Client {
|
func New() *Client {
|
||||||
return &Client{}
|
return &Client{
|
||||||
|
serverAddrs: concatAddrPorts([][]netip.AddrPort{
|
||||||
|
provider.Cloudflare().Plain.IPv4,
|
||||||
|
provider.Google().Plain.IPv4,
|
||||||
|
provider.Quad9().Plain.IPv4,
|
||||||
|
provider.OpenDNS().Plain.IPv4,
|
||||||
|
provider.LibreDNS().Plain.IPv4,
|
||||||
|
provider.Quadrant().Plain.IPv4,
|
||||||
|
provider.CiraProtected().Plain.IPv4,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func concatAddrPorts(addrs [][]netip.AddrPort) []netip.AddrPort {
|
||||||
|
var result []netip.AddrPort
|
||||||
|
for _, addrList := range addrs {
|
||||||
|
result = append(result, addrList...)
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrLookupNoIPs = errors.New("no IPs found from DNS lookup")
|
var ErrLookupNoIPs = errors.New("no IPs found from DNS lookup")
|
||||||
|
|
||||||
func (c *Client) Check(ctx context.Context) error {
|
func (c *Client) Check(ctx context.Context) error {
|
||||||
|
dnsAddr := c.serverAddrs[c.dnsIPIndex].Addr()
|
||||||
resolver := &net.Resolver{
|
resolver := &net.Resolver{
|
||||||
PreferGo: true,
|
PreferGo: true,
|
||||||
Dial: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
Dial: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||||
dialer := net.Dialer{}
|
dialer := net.Dialer{}
|
||||||
return dialer.DialContext(ctx, "udp", "1.1.1.1:53")
|
return dialer.DialContext(ctx, "udp", dnsAddr.String())
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
ips, err := resolver.LookupIP(ctx, "ip", "github.com")
|
ips, err := resolver.LookupIP(ctx, "ip", "github.com")
|
||||||
switch {
|
switch {
|
||||||
case err != nil:
|
case err != nil:
|
||||||
|
c.dnsIPIndex = (c.dnsIPIndex + 1) % len(c.serverAddrs)
|
||||||
return err
|
return err
|
||||||
case len(ips) == 0:
|
case len(ips) == 0:
|
||||||
|
c.dnsIPIndex = (c.dnsIPIndex + 1) % len(c.serverAddrs)
|
||||||
return fmt.Errorf("%w", ErrLookupNoIPs)
|
return fmt.Errorf("%w", ErrLookupNoIPs)
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -62,6 +62,10 @@ func (n *NetLink) LinkSetDown(link Link) (err error) {
|
|||||||
return netlink.LinkSetDown(linkToNetlinkLink(&link))
|
return netlink.LinkSetDown(linkToNetlinkLink(&link))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *NetLink) LinkSetMTU(link Link, mtu int) error {
|
||||||
|
return netlink.LinkSetMTU(linkToNetlinkLink(&link), mtu)
|
||||||
|
}
|
||||||
|
|
||||||
type netlinkLinkImpl struct {
|
type netlinkLinkImpl struct {
|
||||||
attrs *netlink.LinkAttrs
|
attrs *netlink.LinkAttrs
|
||||||
linkType string
|
linkType string
|
||||||
|
|||||||
49
internal/pmtud/apple_ipv4.go
Normal file
49
internal/pmtud/apple_ipv4.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package pmtud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/ipv4"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ net.PacketConn = &ipv4Wrapper{}
|
||||||
|
|
||||||
|
// ipv4Wrapper is a wrapper around ipv4.PacketConn to implement
|
||||||
|
// the net.PacketConn interface. It's only used for Darwin or iOS.
|
||||||
|
type ipv4Wrapper struct {
|
||||||
|
ipv4Conn *ipv4.PacketConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func ipv4ToNetPacketConn(ipv4 *ipv4.PacketConn) *ipv4Wrapper {
|
||||||
|
return &ipv4Wrapper{ipv4Conn: ipv4}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ipv4Wrapper) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||||
|
n, _, addr, err = i.ipv4Conn.ReadFrom(p)
|
||||||
|
return n, addr, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ipv4Wrapper) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||||
|
return i.ipv4Conn.WriteTo(p, nil, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ipv4Wrapper) Close() error {
|
||||||
|
return i.ipv4Conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ipv4Wrapper) LocalAddr() net.Addr {
|
||||||
|
return i.ipv4Conn.LocalAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ipv4Wrapper) SetDeadline(t time.Time) error {
|
||||||
|
return i.ipv4Conn.SetDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ipv4Wrapper) SetReadDeadline(t time.Time) error {
|
||||||
|
return i.ipv4Conn.SetReadDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ipv4Wrapper) SetWriteDeadline(t time.Time) error {
|
||||||
|
return i.ipv4Conn.SetWriteDeadline(t)
|
||||||
|
}
|
||||||
83
internal/pmtud/check.go
Normal file
83
internal/pmtud/check.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package pmtud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/net/icmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrICMPNextHopMTUTooLow = errors.New("ICMP Next Hop MTU is too low")
|
||||||
|
ErrICMPNextHopMTUTooHigh = errors.New("ICMP Next Hop MTU is too high")
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkMTU(mtu, minMTU, physicalLinkMTU int) (err error) {
|
||||||
|
switch {
|
||||||
|
case mtu < minMTU:
|
||||||
|
return fmt.Errorf("%w: %d", ErrICMPNextHopMTUTooLow, mtu)
|
||||||
|
case mtu > physicalLinkMTU:
|
||||||
|
return fmt.Errorf("%w: %d is larger than physical link MTU %d",
|
||||||
|
ErrICMPNextHopMTUTooHigh, mtu, physicalLinkMTU)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkInvokingReplyIDMatch(icmpProtocol int, received []byte,
|
||||||
|
outboundMessage *icmp.Message,
|
||||||
|
) (match bool, err error) {
|
||||||
|
inboundMessage, err := icmp.ParseMessage(icmpProtocol, received)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("parsing invoking packet: %w", err)
|
||||||
|
}
|
||||||
|
inboundBody, ok := inboundMessage.Body.(*icmp.Echo)
|
||||||
|
if !ok {
|
||||||
|
return false, fmt.Errorf("%w: %T", ErrICMPBodyUnsupported, inboundMessage.Body)
|
||||||
|
}
|
||||||
|
outboundBody := outboundMessage.Body.(*icmp.Echo) //nolint:forcetypeassert
|
||||||
|
return inboundBody.ID == outboundBody.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrICMPIDMismatch = errors.New("ICMP id mismatch")
|
||||||
|
|
||||||
|
func checkEchoReply(icmpProtocol int, received []byte,
|
||||||
|
outboundMessage *icmp.Message, truncatedBody bool,
|
||||||
|
) (err error) {
|
||||||
|
inboundMessage, err := icmp.ParseMessage(icmpProtocol, received)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parsing invoking packet: %w", err)
|
||||||
|
}
|
||||||
|
inboundBody, ok := inboundMessage.Body.(*icmp.Echo)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("%w: %T", ErrICMPBodyUnsupported, inboundMessage.Body)
|
||||||
|
}
|
||||||
|
outboundBody := outboundMessage.Body.(*icmp.Echo) //nolint:forcetypeassert
|
||||||
|
if inboundBody.ID != outboundBody.ID {
|
||||||
|
return fmt.Errorf("%w: sent id %d and received id %d",
|
||||||
|
ErrICMPIDMismatch, outboundBody.ID, inboundBody.ID)
|
||||||
|
}
|
||||||
|
err = checkEchoBodies(outboundBody.Data, inboundBody.Data, truncatedBody)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("checking sent and received bodies: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrICMPEchoDataMismatch = errors.New("ICMP data mismatch")
|
||||||
|
|
||||||
|
func checkEchoBodies(sent, received []byte, receivedTruncated bool) (err error) {
|
||||||
|
if len(received) > len(sent) {
|
||||||
|
return fmt.Errorf("%w: sent %d bytes and received %d bytes",
|
||||||
|
ErrICMPEchoDataMismatch, len(sent), len(received))
|
||||||
|
}
|
||||||
|
if receivedTruncated {
|
||||||
|
sent = sent[:len(received)]
|
||||||
|
}
|
||||||
|
if !bytes.Equal(received, sent) {
|
||||||
|
return fmt.Errorf("%w: sent %x and received %x",
|
||||||
|
ErrICMPEchoDataMismatch, sent, received)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
10
internal/pmtud/df.go
Normal file
10
internal/pmtud/df.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
//go:build !linux && !windows
|
||||||
|
|
||||||
|
package pmtud
|
||||||
|
|
||||||
|
// setDontFragment for platforms other than Linux and Windows
|
||||||
|
// is not implemented, so we just return assuming the don't
|
||||||
|
// fragment flag is set on IP packets.
|
||||||
|
func setDontFragment(fd uintptr) (err error) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
12
internal/pmtud/df_linux.go
Normal file
12
internal/pmtud/df_linux.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package pmtud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setDontFragment(fd uintptr) (err error) {
|
||||||
|
return syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP,
|
||||||
|
syscall.IP_MTU_DISCOVER, syscall.IP_PMTUDISC_PROBE)
|
||||||
|
}
|
||||||
13
internal/pmtud/df_windows.go
Normal file
13
internal/pmtud/df_windows.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package pmtud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setDontFragment(fd uintptr) (err error) {
|
||||||
|
// https://docs.microsoft.com/en-us/troubleshoot/windows/win32/header-library-requirement-socket-ipproto-ip
|
||||||
|
// #define IP_DONTFRAGMENT 14 /* don't fragment IP datagrams */
|
||||||
|
return syscall.SetsockoptInt(syscall.Handle(fd), syscall.IPPROTO_IP, 14, 1)
|
||||||
|
}
|
||||||
29
internal/pmtud/errors.go
Normal file
29
internal/pmtud/errors.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package pmtud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrICMPNotPermitted = errors.New("ICMP not permitted")
|
||||||
|
ErrICMPDestinationUnreachable = errors.New("ICMP destination unreachable")
|
||||||
|
ErrICMPCommunicationAdministrativelyProhibited = errors.New("communication administratively prohibited")
|
||||||
|
ErrICMPBodyUnsupported = errors.New("ICMP body type is not supported")
|
||||||
|
)
|
||||||
|
|
||||||
|
func wrapConnErr(err error, timedCtx context.Context, pingTimeout time.Duration) error { //nolint:revive
|
||||||
|
switch {
|
||||||
|
case strings.HasSuffix(err.Error(), "sendto: operation not permitted"):
|
||||||
|
err = fmt.Errorf("%w", ErrICMPNotPermitted)
|
||||||
|
case errors.Is(timedCtx.Err(), context.DeadlineExceeded):
|
||||||
|
err = fmt.Errorf("%w (timed out after %s)", net.ErrClosed, pingTimeout)
|
||||||
|
case timedCtx.Err() != nil:
|
||||||
|
err = timedCtx.Err()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
7
internal/pmtud/interfaces.go
Normal file
7
internal/pmtud/interfaces.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package pmtud
|
||||||
|
|
||||||
|
type Logger interface {
|
||||||
|
Debug(msg string)
|
||||||
|
Debugf(msg string, args ...any)
|
||||||
|
Warnf(msg string, args ...any)
|
||||||
|
}
|
||||||
159
internal/pmtud/ipv4.go
Normal file
159
internal/pmtud/ipv4.go
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
package pmtud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/icmp"
|
||||||
|
"golang.org/x/net/ipv4"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// see https://en.wikipedia.org/wiki/Maximum_transmission_unit#MTUs_for_common_media
|
||||||
|
minIPv4MTU = 68
|
||||||
|
icmpv4Protocol = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
func listenICMPv4(ctx context.Context) (conn net.PacketConn, err error) {
|
||||||
|
var listenConfig net.ListenConfig
|
||||||
|
listenConfig.Control = func(_, _ string, rawConn syscall.RawConn) error {
|
||||||
|
var setDFErr error
|
||||||
|
err := rawConn.Control(func(fd uintptr) {
|
||||||
|
setDFErr = setDontFragment(fd) // runs when calling ListenPacket
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
err = setDFErr
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const listenAddress = ""
|
||||||
|
packetConn, err := listenConfig.ListenPacket(ctx, "ip4:icmp", listenAddress)
|
||||||
|
if err != nil {
|
||||||
|
if strings.HasSuffix(err.Error(), "socket: operation not permitted") {
|
||||||
|
err = fmt.Errorf("%w: you can try adding NET_RAW capability to resolve this", ErrICMPNotPermitted)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
|
||||||
|
packetConn = ipv4ToNetPacketConn(ipv4.NewPacketConn(packetConn))
|
||||||
|
}
|
||||||
|
|
||||||
|
return packetConn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findIPv4NextHopMTU(ctx context.Context, ip netip.Addr,
|
||||||
|
physicalLinkMTU int, pingTimeout time.Duration, logger Logger,
|
||||||
|
) (mtu int, err error) {
|
||||||
|
if ip.Is6() {
|
||||||
|
panic("IP address is not v4")
|
||||||
|
}
|
||||||
|
conn, err := listenICMPv4(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("listening for ICMP packets: %w", err)
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, pingTimeout)
|
||||||
|
defer cancel()
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
conn.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// First try to send a packet which is too big to get the maximum MTU
|
||||||
|
// directly.
|
||||||
|
outboundID, outboundMessage := buildMessageToSend("v4", physicalLinkMTU)
|
||||||
|
encodedMessage, err := outboundMessage.Marshal(nil)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("encoding ICMP message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = conn.WriteTo(encodedMessage, &net.IPAddr{IP: ip.AsSlice()})
|
||||||
|
if err != nil {
|
||||||
|
err = wrapConnErr(err, ctx, pingTimeout)
|
||||||
|
return 0, fmt.Errorf("writing ICMP message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := make([]byte, physicalLinkMTU)
|
||||||
|
|
||||||
|
for { // for loop in case we read an echo reply for another ICMP request
|
||||||
|
// Note we need to read the whole packet in one call to ReadFrom, so the buffer
|
||||||
|
// must be large enough to read the entire reply packet. See:
|
||||||
|
// https://groups.google.com/g/golang-nuts/c/5dy2Q4nPs08/m/KmuSQAGEtG4J
|
||||||
|
bytesRead, _, err := conn.ReadFrom(buffer)
|
||||||
|
if err != nil {
|
||||||
|
err = wrapConnErr(err, ctx, pingTimeout)
|
||||||
|
return 0, fmt.Errorf("reading from ICMP connection: %w", err)
|
||||||
|
}
|
||||||
|
packetBytes := buffer[:bytesRead]
|
||||||
|
// Side note: echo reply should be at most the number of bytes
|
||||||
|
// sent, and can be lower, more precisely 576-ipHeader bytes,
|
||||||
|
// in case the next hop we are reaching replies with a destination
|
||||||
|
// unreachable and wants to ensure the response makes it way back
|
||||||
|
// by keeping a low packet size, see:
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc1122#page-59
|
||||||
|
|
||||||
|
inboundMessage, err := icmp.ParseMessage(icmpv4Protocol, packetBytes)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("parsing message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch typedBody := inboundMessage.Body.(type) {
|
||||||
|
case *icmp.DstUnreach:
|
||||||
|
const fragmentationRequiredAndDFFlagSetCode = 4
|
||||||
|
const communicationAdministrativelyProhibitedCode = 13
|
||||||
|
switch inboundMessage.Code {
|
||||||
|
case fragmentationRequiredAndDFFlagSetCode:
|
||||||
|
case communicationAdministrativelyProhibitedCode:
|
||||||
|
return 0, fmt.Errorf("%w: %w (code %d)",
|
||||||
|
ErrICMPDestinationUnreachable,
|
||||||
|
ErrICMPCommunicationAdministrativelyProhibited,
|
||||||
|
inboundMessage.Code)
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("%w: code %d",
|
||||||
|
ErrICMPDestinationUnreachable, inboundMessage.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// See https://datatracker.ietf.org/doc/html/rfc1191#section-4
|
||||||
|
// Note: the go library does not handle this NextHopMTU section.
|
||||||
|
nextHopMTU := packetBytes[6:8]
|
||||||
|
mtu = int(binary.BigEndian.Uint16(nextHopMTU))
|
||||||
|
err = checkMTU(mtu, minIPv4MTU, physicalLinkMTU)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("checking next-hop-mtu found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The code below is really for sanity checks
|
||||||
|
packetBytes = packetBytes[8:]
|
||||||
|
header, err := ipv4.ParseHeader(packetBytes)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("parsing IPv4 header: %w", err)
|
||||||
|
}
|
||||||
|
packetBytes = packetBytes[header.Len:] // truncated original datagram
|
||||||
|
|
||||||
|
const truncated = true
|
||||||
|
err = checkEchoReply(icmpv4Protocol, packetBytes, outboundMessage, truncated)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("checking echo reply: %w", err)
|
||||||
|
}
|
||||||
|
return mtu, nil
|
||||||
|
case *icmp.Echo:
|
||||||
|
inboundID := uint16(typedBody.ID) //nolint:gosec
|
||||||
|
if inboundID == outboundID {
|
||||||
|
return physicalLinkMTU, nil
|
||||||
|
}
|
||||||
|
logger.Debugf("discarding received ICMP echo reply with id %d mismatching sent id %d",
|
||||||
|
inboundID, outboundID)
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("%w: %T", ErrICMPBodyUnsupported, typedBody)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
122
internal/pmtud/ipv6.go
Normal file
122
internal/pmtud/ipv6.go
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
package pmtud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/icmp"
|
||||||
|
"golang.org/x/net/ipv6"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
minIPv6MTU = 1280
|
||||||
|
icmpv6Protocol = 58
|
||||||
|
)
|
||||||
|
|
||||||
|
func listenICMPv6(ctx context.Context) (conn net.PacketConn, err error) {
|
||||||
|
var listenConfig net.ListenConfig
|
||||||
|
const listenAddress = ""
|
||||||
|
packetConn, err := listenConfig.ListenPacket(ctx, "ip6:ipv6-icmp", listenAddress)
|
||||||
|
if err != nil {
|
||||||
|
if strings.HasSuffix(err.Error(), "socket: operation not permitted") {
|
||||||
|
err = fmt.Errorf("%w: you can try adding NET_RAW capability to resolve this", ErrICMPNotPermitted)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return packetConn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIPv6PacketTooBig(ctx context.Context, ip netip.Addr,
|
||||||
|
physicalLinkMTU int, pingTimeout time.Duration, logger Logger,
|
||||||
|
) (mtu int, err error) {
|
||||||
|
if ip.Is4() {
|
||||||
|
panic("IP address is not v6")
|
||||||
|
}
|
||||||
|
conn, err := listenICMPv6(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("listening for ICMP packets: %w", err)
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, pingTimeout)
|
||||||
|
defer cancel()
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
conn.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// First try to send a packet which is too big to get the maximum MTU
|
||||||
|
// directly.
|
||||||
|
outboundID, outboundMessage := buildMessageToSend("v6", physicalLinkMTU)
|
||||||
|
encodedMessage, err := outboundMessage.Marshal(nil)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("encoding ICMP message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = conn.WriteTo(encodedMessage, &net.IPAddr{IP: ip.AsSlice(), Zone: ip.Zone()})
|
||||||
|
if err != nil {
|
||||||
|
err = wrapConnErr(err, ctx, pingTimeout)
|
||||||
|
return 0, fmt.Errorf("writing ICMP message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := make([]byte, physicalLinkMTU)
|
||||||
|
|
||||||
|
for { // for loop if we encounter another ICMP packet with an unknown id.
|
||||||
|
// Note we need to read the whole packet in one call to ReadFrom, so the buffer
|
||||||
|
// must be large enough to read the entire reply packet. See:
|
||||||
|
// https://groups.google.com/g/golang-nuts/c/5dy2Q4nPs08/m/KmuSQAGEtG4J
|
||||||
|
bytesRead, _, err := conn.ReadFrom(buffer)
|
||||||
|
if err != nil {
|
||||||
|
err = wrapConnErr(err, ctx, pingTimeout)
|
||||||
|
return 0, fmt.Errorf("reading from ICMP connection: %w", err)
|
||||||
|
}
|
||||||
|
packetBytes := buffer[:bytesRead]
|
||||||
|
|
||||||
|
packetBytes = packetBytes[ipv6.HeaderLen:]
|
||||||
|
|
||||||
|
inboundMessage, err := icmp.ParseMessage(icmpv6Protocol, packetBytes)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("parsing message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch typedBody := inboundMessage.Body.(type) {
|
||||||
|
case *icmp.PacketTooBig:
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc1885#section-3.2
|
||||||
|
mtu = typedBody.MTU
|
||||||
|
err = checkMTU(mtu, minIPv6MTU, physicalLinkMTU)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("checking MTU: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity checks
|
||||||
|
const truncatedBody = true
|
||||||
|
err = checkEchoReply(icmpv6Protocol, typedBody.Data, outboundMessage, truncatedBody)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("checking invoking message: %w", err)
|
||||||
|
}
|
||||||
|
return typedBody.MTU, nil
|
||||||
|
case *icmp.DstUnreach:
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc1885#section-3.1
|
||||||
|
idMatch, err := checkInvokingReplyIDMatch(icmpv6Protocol, packetBytes, outboundMessage)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("checking invoking message id: %w", err)
|
||||||
|
} else if idMatch {
|
||||||
|
return 0, fmt.Errorf("%w", ErrICMPDestinationUnreachable)
|
||||||
|
}
|
||||||
|
logger.Debug("discarding received ICMP destination unreachable reply with an unknown id")
|
||||||
|
continue
|
||||||
|
case *icmp.Echo:
|
||||||
|
inboundID := uint16(typedBody.ID) //nolint:gosec
|
||||||
|
if inboundID == outboundID {
|
||||||
|
return physicalLinkMTU, nil
|
||||||
|
}
|
||||||
|
logger.Debugf("discarding received ICMP echo reply with id %d mismatching sent id %d",
|
||||||
|
inboundID, outboundID)
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("%w: %T", ErrICMPBodyUnsupported, typedBody)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
58
internal/pmtud/message.go
Normal file
58
internal/pmtud/message.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package pmtud
|
||||||
|
|
||||||
|
import (
|
||||||
|
cryptorand "crypto/rand"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"math/rand/v2"
|
||||||
|
|
||||||
|
"golang.org/x/net/icmp"
|
||||||
|
"golang.org/x/net/ipv4"
|
||||||
|
"golang.org/x/net/ipv6"
|
||||||
|
)
|
||||||
|
|
||||||
|
func buildMessageToSend(ipVersion string, mtu int) (id uint16, message *icmp.Message) {
|
||||||
|
var seed [32]byte
|
||||||
|
_, _ = cryptorand.Read(seed[:])
|
||||||
|
randomSource := rand.NewChaCha8(seed)
|
||||||
|
|
||||||
|
const uint16Bytes = 2
|
||||||
|
idBytes := make([]byte, uint16Bytes)
|
||||||
|
_, _ = randomSource.Read(idBytes)
|
||||||
|
id = binary.BigEndian.Uint16(idBytes)
|
||||||
|
|
||||||
|
var ipHeaderLength int
|
||||||
|
var icmpType icmp.Type
|
||||||
|
switch ipVersion {
|
||||||
|
case "v4":
|
||||||
|
ipHeaderLength = ipv4.HeaderLen
|
||||||
|
icmpType = ipv4.ICMPTypeEcho
|
||||||
|
case "v6":
|
||||||
|
ipHeaderLength = ipv6.HeaderLen
|
||||||
|
icmpType = ipv6.ICMPTypeEchoRequest
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("IP version %q not supported", ipVersion))
|
||||||
|
}
|
||||||
|
const pingHeaderLength = 0 +
|
||||||
|
1 + // type
|
||||||
|
1 + // code
|
||||||
|
2 + // checksum
|
||||||
|
2 + // identifier
|
||||||
|
2 // sequence number
|
||||||
|
pingBodyDataSize := mtu - ipHeaderLength - pingHeaderLength
|
||||||
|
messageBodyData := make([]byte, pingBodyDataSize)
|
||||||
|
_, _ = randomSource.Read(messageBodyData)
|
||||||
|
|
||||||
|
// See https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml#icmp-parameters-types
|
||||||
|
message = &icmp.Message{
|
||||||
|
Type: icmpType, // echo request
|
||||||
|
Code: 0, // no code
|
||||||
|
Checksum: 0, // calculated at encoding (ipv4) or sending (ipv6)
|
||||||
|
Body: &icmp.Echo{
|
||||||
|
ID: int(id),
|
||||||
|
Seq: 0, // only one packet
|
||||||
|
Data: messageBodyData,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return id, message
|
||||||
|
}
|
||||||
7
internal/pmtud/nooplogger.go
Normal file
7
internal/pmtud/nooplogger.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package pmtud
|
||||||
|
|
||||||
|
type noopLogger struct{}
|
||||||
|
|
||||||
|
func (noopLogger) Debug(_ string) {}
|
||||||
|
func (noopLogger) Debugf(_ string, _ ...any) {}
|
||||||
|
func (noopLogger) Warnf(_ string, _ ...any) {}
|
||||||
271
internal/pmtud/pmtud.go
Normal file
271
internal/pmtud/pmtud.go
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
package pmtud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/icmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrMTUNotFound = errors.New("path MTU discovery failed to find MTU")
|
||||||
|
|
||||||
|
// PathMTUDiscover discovers the maximum MTU for the path to the given ip address.
|
||||||
|
// If the physicalLinkMTU is zero, it defaults to 1500 which is the ethernet standard MTU.
|
||||||
|
// If the pingTimeout is zero, it defaults to 1 second.
|
||||||
|
// If the logger is nil, a no-op logger is used.
|
||||||
|
// It returns [ErrMTUNotFound] if the MTU could not be determined.
|
||||||
|
func PathMTUDiscover(ctx context.Context, ip netip.Addr,
|
||||||
|
physicalLinkMTU int, pingTimeout time.Duration, logger Logger) (
|
||||||
|
mtu int, err error,
|
||||||
|
) {
|
||||||
|
if physicalLinkMTU == 0 {
|
||||||
|
const ethernetStandardMTU = 1500
|
||||||
|
physicalLinkMTU = ethernetStandardMTU
|
||||||
|
}
|
||||||
|
if pingTimeout == 0 {
|
||||||
|
pingTimeout = time.Second
|
||||||
|
}
|
||||||
|
if logger == nil {
|
||||||
|
logger = &noopLogger{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip.Is4() {
|
||||||
|
logger.Debug("finding IPv4 next hop MTU")
|
||||||
|
mtu, err = findIPv4NextHopMTU(ctx, ip, physicalLinkMTU, pingTimeout, logger)
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
return mtu, nil
|
||||||
|
case errors.Is(err, net.ErrClosed) || errors.Is(err, ErrICMPCommunicationAdministrativelyProhibited): // blackhole
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("finding IPv4 next hop MTU: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.Debug("requesting IPv6 ICMP packet-too-big reply")
|
||||||
|
mtu, err = getIPv6PacketTooBig(ctx, ip, physicalLinkMTU, pingTimeout, logger)
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
return mtu, nil
|
||||||
|
case errors.Is(err, net.ErrClosed): // blackhole
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("getting IPv6 packet-too-big message: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back method: send echo requests with different packet
|
||||||
|
// sizes and check which ones succeed to find the maximum MTU.
|
||||||
|
logger.Debug("falling back to sending different sized echo packets")
|
||||||
|
minMTU := minIPv4MTU
|
||||||
|
if ip.Is6() {
|
||||||
|
minMTU = minIPv6MTU
|
||||||
|
}
|
||||||
|
return pmtudMultiSizes(ctx, ip, minMTU, physicalLinkMTU, pingTimeout, logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
type pmtudTestUnit struct {
|
||||||
|
mtu int
|
||||||
|
echoID uint16
|
||||||
|
sentBytes int
|
||||||
|
ok bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func pmtudMultiSizes(ctx context.Context, ip netip.Addr,
|
||||||
|
minMTU, maxPossibleMTU int, pingTimeout time.Duration,
|
||||||
|
logger Logger,
|
||||||
|
) (maxMTU int, err error) {
|
||||||
|
var ipVersion string
|
||||||
|
var conn net.PacketConn
|
||||||
|
if ip.Is4() {
|
||||||
|
ipVersion = "v4"
|
||||||
|
conn, err = listenICMPv4(ctx)
|
||||||
|
} else {
|
||||||
|
ipVersion = "v6"
|
||||||
|
conn, err = listenICMPv6(ctx)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
if strings.HasSuffix(err.Error(), "socket: operation not permitted") {
|
||||||
|
err = fmt.Errorf("%w: you can try adding NET_RAW capability to resolve this", ErrICMPNotPermitted)
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("listening for ICMP packets: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mtusToTest := makeMTUsToTest(minMTU, maxPossibleMTU)
|
||||||
|
if len(mtusToTest) == 1 { // only minMTU because minMTU == maxPossibleMTU
|
||||||
|
return minMTU, nil
|
||||||
|
}
|
||||||
|
logger.Debugf("testing the following MTUs: %v", mtusToTest)
|
||||||
|
|
||||||
|
tests := make([]pmtudTestUnit, len(mtusToTest))
|
||||||
|
for i := range mtusToTest {
|
||||||
|
tests[i] = pmtudTestUnit{mtu: mtusToTest[i]}
|
||||||
|
}
|
||||||
|
|
||||||
|
timedCtx, cancel := context.WithTimeout(ctx, pingTimeout)
|
||||||
|
defer cancel()
|
||||||
|
go func() {
|
||||||
|
<-timedCtx.Done()
|
||||||
|
conn.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
for i := range tests {
|
||||||
|
id, message := buildMessageToSend(ipVersion, tests[i].mtu)
|
||||||
|
tests[i].echoID = id
|
||||||
|
|
||||||
|
encodedMessage, err := message.Marshal(nil)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("encoding ICMP message: %w", err)
|
||||||
|
}
|
||||||
|
tests[i].sentBytes = len(encodedMessage)
|
||||||
|
|
||||||
|
_, err = conn.WriteTo(encodedMessage, &net.IPAddr{IP: ip.AsSlice()})
|
||||||
|
if err != nil {
|
||||||
|
if strings.HasSuffix(err.Error(), "sendto: operation not permitted") {
|
||||||
|
err = fmt.Errorf("%w", ErrICMPNotPermitted)
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("writing ICMP message: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = collectReplies(conn, ipVersion, tests, logger)
|
||||||
|
switch {
|
||||||
|
case err == nil: // max possible MTU is working
|
||||||
|
return tests[len(tests)-1].mtu, nil
|
||||||
|
case err != nil && errors.Is(err, net.ErrClosed):
|
||||||
|
// we have timeouts (IPv4 testing or IPv6 PMTUD blackholes)
|
||||||
|
// so find the highest MTU which worked.
|
||||||
|
// Note we start from index len(tests) - 2 since the max MTU
|
||||||
|
// cannot be working if we had a timeout.
|
||||||
|
for i := len(tests) - 2; i >= 0; i-- { //nolint:mnd
|
||||||
|
if tests[i].ok {
|
||||||
|
return pmtudMultiSizes(ctx, ip, tests[i].mtu, tests[i+1].mtu-1,
|
||||||
|
pingTimeout, logger)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All MTUs failed.
|
||||||
|
return 0, fmt.Errorf("%w: ICMP might be blocked", ErrMTUNotFound)
|
||||||
|
case err != nil:
|
||||||
|
return 0, fmt.Errorf("collecting ICMP echo replies: %w", err)
|
||||||
|
default:
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the MTU slice of length 11 such that:
|
||||||
|
// - the first element is the minMTU
|
||||||
|
// - the last element is the maxMTU
|
||||||
|
// - elements in-between are separated as close to each other
|
||||||
|
// The number 11 is chosen to find the final MTU in 3 searches,
|
||||||
|
// with a total search space of 1728 MTUs which is enough;
|
||||||
|
// to find it in 2 searches requires 37 parallel queries which
|
||||||
|
// could be blocked by firewalls.
|
||||||
|
func makeMTUsToTest(minMTU, maxMTU int) (mtus []int) {
|
||||||
|
const mtusLength = 11 // find the final MTU in 3 searches
|
||||||
|
diff := maxMTU - minMTU
|
||||||
|
switch {
|
||||||
|
case minMTU > maxMTU:
|
||||||
|
panic("minMTU > maxMTU")
|
||||||
|
case diff <= mtusLength:
|
||||||
|
mtus = make([]int, 0, diff)
|
||||||
|
for mtu := minMTU; mtu <= maxMTU; mtu++ {
|
||||||
|
mtus = append(mtus, mtu)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
step := float64(diff) / float64(mtusLength-1)
|
||||||
|
mtus = make([]int, 0, mtusLength)
|
||||||
|
for mtu := float64(minMTU); len(mtus) < mtusLength-1; mtu += step {
|
||||||
|
mtus = append(mtus, int(math.Round(mtu)))
|
||||||
|
}
|
||||||
|
mtus = append(mtus, maxMTU) // last element is the maxMTU
|
||||||
|
}
|
||||||
|
|
||||||
|
return mtus
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectReplies(conn net.PacketConn, ipVersion string,
|
||||||
|
tests []pmtudTestUnit, logger Logger,
|
||||||
|
) (err error) {
|
||||||
|
echoIDToTestIndex := make(map[uint16]int, len(tests))
|
||||||
|
for i, test := range tests {
|
||||||
|
echoIDToTestIndex[test.echoID] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
// The theoretical limit is 4GiB for IPv6 MTU path discovery jumbograms, but that would
|
||||||
|
// create huge buffers which we don't really want to support anyway.
|
||||||
|
// The standard frame maximum MTU is 1500 bytes, and there are Jumbo frames with
|
||||||
|
// a conventional maximum of 9000 bytes. However, some manufacturers support up
|
||||||
|
// 9216-20 = 9196 bytes for the maximum MTU. We thus use buffers of size 9196 to
|
||||||
|
// match eventual Jumbo frames. More information at:
|
||||||
|
// https://en.wikipedia.org/wiki/Maximum_transmission_unit#MTUs_for_common_media
|
||||||
|
const maxPossibleMTU = 9196
|
||||||
|
buffer := make([]byte, maxPossibleMTU)
|
||||||
|
|
||||||
|
idsFound := 0
|
||||||
|
for idsFound < len(tests) {
|
||||||
|
// Note we need to read the whole packet in one call to ReadFrom, so the buffer
|
||||||
|
// must be large enough to read the entire reply packet. See:
|
||||||
|
// https://groups.google.com/g/golang-nuts/c/5dy2Q4nPs08/m/KmuSQAGEtG4J
|
||||||
|
bytesRead, _, err := conn.ReadFrom(buffer)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading from ICMP connection: %w", err)
|
||||||
|
}
|
||||||
|
packetBytes := buffer[:bytesRead]
|
||||||
|
|
||||||
|
ipPacketLength := len(packetBytes)
|
||||||
|
|
||||||
|
var icmpProtocol int
|
||||||
|
switch ipVersion {
|
||||||
|
case "v4":
|
||||||
|
icmpProtocol = icmpv4Protocol
|
||||||
|
case "v6":
|
||||||
|
icmpProtocol = icmpv6Protocol
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unknown IP version: %s", ipVersion))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the ICMP message
|
||||||
|
// Note: this parsing works for a truncated 556 bytes ICMP reply packet.
|
||||||
|
message, err := icmp.ParseMessage(icmpProtocol, packetBytes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parsing message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
echoBody, ok := message.Body.(*icmp.Echo)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("%w: %T", ErrICMPBodyUnsupported, message.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
id := uint16(echoBody.ID) //nolint:gosec
|
||||||
|
testIndex, testing := echoIDToTestIndex[id]
|
||||||
|
if !testing { // not an id we expected so ignore it
|
||||||
|
logger.Warnf("ignoring ICMP reply with unexpected ID %d (type: %d, code: %d, length: %d)",
|
||||||
|
echoBody.ID, message.Type, message.Code, ipPacketLength)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
idsFound++
|
||||||
|
sentBytes := tests[testIndex].sentBytes
|
||||||
|
|
||||||
|
// echo reply should be at most the number of bytes sent,
|
||||||
|
// and can be lower, more precisely 556 bytes, in case
|
||||||
|
// the host we are reaching wants to stay out of trouble
|
||||||
|
// and ensure its echo reply goes through without
|
||||||
|
// fragmentation, see the following page:
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc1122#page-59
|
||||||
|
const conservativeReplyLength = 556
|
||||||
|
truncated := ipPacketLength < sentBytes &&
|
||||||
|
ipPacketLength == conservativeReplyLength
|
||||||
|
// Check the packet size is the same if the reply is not truncated
|
||||||
|
if !truncated && sentBytes != ipPacketLength {
|
||||||
|
return fmt.Errorf("%w: sent %dB and received %dB",
|
||||||
|
ErrICMPEchoDataMismatch, sentBytes, ipPacketLength)
|
||||||
|
}
|
||||||
|
// Truncated reply or matching reply size
|
||||||
|
tests[testIndex].ok = true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
22
internal/pmtud/pmtud_integration_test.go
Normal file
22
internal/pmtud/pmtud_integration_test.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
//go:build integration
|
||||||
|
|
||||||
|
package pmtud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_PathMTUDiscover(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
const physicalLinkMTU = 1500
|
||||||
|
const timeout = time.Second
|
||||||
|
mtu, err := PathMTUDiscover(context.Background(), netip.MustParseAddr("1.1.1.1"),
|
||||||
|
physicalLinkMTU, timeout, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Log("MTU found:", mtu)
|
||||||
|
}
|
||||||
55
internal/pmtud/pmtud_test.go
Normal file
55
internal/pmtud/pmtud_test.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package pmtud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_makeMTUsToTest(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
minMTU int
|
||||||
|
maxMTU int
|
||||||
|
mtus []int
|
||||||
|
}{
|
||||||
|
"0_0": {
|
||||||
|
mtus: []int{0},
|
||||||
|
},
|
||||||
|
"0_1": {
|
||||||
|
maxMTU: 1,
|
||||||
|
mtus: []int{0, 1},
|
||||||
|
},
|
||||||
|
"0_8": {
|
||||||
|
maxMTU: 8,
|
||||||
|
mtus: []int{0, 1, 2, 3, 4, 5, 6, 7, 8},
|
||||||
|
},
|
||||||
|
"0_12": {
|
||||||
|
maxMTU: 12,
|
||||||
|
mtus: []int{0, 1, 2, 4, 5, 6, 7, 8, 10, 11, 12},
|
||||||
|
},
|
||||||
|
"0_80": {
|
||||||
|
maxMTU: 80,
|
||||||
|
mtus: []int{0, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80},
|
||||||
|
},
|
||||||
|
"0_100": {
|
||||||
|
maxMTU: 100,
|
||||||
|
mtus: []int{0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100},
|
||||||
|
},
|
||||||
|
"1280_1500": {
|
||||||
|
minMTU: 1280,
|
||||||
|
maxMTU: 1500,
|
||||||
|
mtus: []int{1280, 1302, 1324, 1346, 1368, 1390, 1412, 1434, 1456, 1478, 1500},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
mtus := makeMTUsToTest(testCase.minMTU, testCase.maxMTU)
|
||||||
|
assert.Equal(t, testCase.mtus, mtus)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,12 +15,12 @@ type Provider struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func New(storage common.Storage, randSource rand.Source,
|
func New(storage common.Storage, randSource rand.Source,
|
||||||
parallelResolver common.ParallelResolver,
|
updaterWarner common.Warner, parallelResolver common.ParallelResolver,
|
||||||
) *Provider {
|
) *Provider {
|
||||||
return &Provider{
|
return &Provider{
|
||||||
storage: storage,
|
storage: storage,
|
||||||
randSource: randSource,
|
randSource: randSource,
|
||||||
Fetcher: updater.New(parallelResolver),
|
Fetcher: updater.New(parallelResolver, updaterWarner),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ func (u *Updater) FetchServers(ctx context.Context, minServers int) (
|
|||||||
|
|
||||||
possibleHosts := possibleServers.hostsSlice()
|
possibleHosts := possibleServers.hostsSlice()
|
||||||
resolveSettings := parallelResolverSettings(possibleHosts)
|
resolveSettings := parallelResolverSettings(possibleHosts)
|
||||||
hostToIPs, _, err := u.parallelResolver.Resolve(ctx, resolveSettings)
|
hostToIPs, warnings, err := u.parallelResolver.Resolve(ctx, resolveSettings)
|
||||||
|
for _, warning := range warnings {
|
||||||
|
u.warner.Warn(warning)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import (
|
|||||||
|
|
||||||
type Updater struct {
|
type Updater struct {
|
||||||
parallelResolver common.ParallelResolver
|
parallelResolver common.ParallelResolver
|
||||||
|
warner common.Warner
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(parallelResolver common.ParallelResolver) *Updater {
|
func New(parallelResolver common.ParallelResolver, warner common.Warner) *Updater {
|
||||||
return &Updater{
|
return &Updater{
|
||||||
parallelResolver: parallelResolver,
|
parallelResolver: parallelResolver,
|
||||||
|
warner: warner,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ func NewProviders(storage Storage, timeNow func() time.Time,
|
|||||||
providerNameToProvider := map[string]Provider{
|
providerNameToProvider := map[string]Provider{
|
||||||
providers.Airvpn: airvpn.New(storage, randSource, client),
|
providers.Airvpn: airvpn.New(storage, randSource, client),
|
||||||
providers.Custom: custom.New(extractor),
|
providers.Custom: custom.New(extractor),
|
||||||
providers.Cyberghost: cyberghost.New(storage, randSource, parallelResolver),
|
providers.Cyberghost: cyberghost.New(storage, randSource, updaterWarner, parallelResolver),
|
||||||
providers.Expressvpn: expressvpn.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
|
providers.Expressvpn: expressvpn.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
|
||||||
providers.Fastestvpn: fastestvpn.New(storage, randSource, client, updaterWarner, parallelResolver),
|
providers.Fastestvpn: fastestvpn.New(storage, randSource, client, updaterWarner, parallelResolver),
|
||||||
providers.Giganews: giganews.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
|
providers.Giganews: giganews.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type infoWarner interface {
|
|||||||
|
|
||||||
type infoer interface {
|
type infoer interface {
|
||||||
Info(s string)
|
Info(s string)
|
||||||
|
Infof(format string, args ...any)
|
||||||
}
|
}
|
||||||
|
|
||||||
type warner interface {
|
type warner interface {
|
||||||
|
|||||||
@@ -13,9 +13,6 @@ import (
|
|||||||
func Read(filepath string) (settings Settings, err error) {
|
func Read(filepath string) (settings Settings, err error) {
|
||||||
file, err := os.Open(filepath)
|
file, err := os.Open(filepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
|
||||||
return Settings{}, nil
|
|
||||||
}
|
|
||||||
return settings, fmt.Errorf("opening file: %w", err)
|
return settings, fmt.Errorf("opening file: %w", err)
|
||||||
}
|
}
|
||||||
decoder := toml.NewDecoder(file)
|
decoder := toml.NewDecoder(file)
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/httpserver"
|
"github.com/qdm12/gluetun/internal/httpserver"
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
"github.com/qdm12/gluetun/internal/models"
|
||||||
@@ -17,8 +19,12 @@ func New(ctx context.Context, address string, logEnabled bool, logger Logger,
|
|||||||
server *httpserver.Server, err error,
|
server *httpserver.Server, err error,
|
||||||
) {
|
) {
|
||||||
authSettings, err := auth.Read(authConfigPath)
|
authSettings, err := auth.Read(authConfigPath)
|
||||||
if err != nil {
|
switch {
|
||||||
|
case errors.Is(err, os.ErrNotExist): // no auth file present
|
||||||
|
case err != nil:
|
||||||
return nil, fmt.Errorf("reading auth settings: %w", err)
|
return nil, fmt.Errorf("reading auth settings: %w", err)
|
||||||
|
default:
|
||||||
|
logger.Infof("read %d roles from authentication file", len(authSettings.Roles))
|
||||||
}
|
}
|
||||||
authSettings.SetDefaults()
|
authSettings.SetDefaults()
|
||||||
err = authSettings.Validate()
|
err = authSettings.Validate()
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -81,6 +81,7 @@ type Linker interface {
|
|||||||
LinkDel(link netlink.Link) (err error)
|
LinkDel(link netlink.Link) (err error)
|
||||||
LinkSetUp(link netlink.Link) (linkIndex int, err error)
|
LinkSetUp(link netlink.Link) (linkIndex int, err error)
|
||||||
LinkSetDown(link netlink.Link) (err error)
|
LinkSetDown(link netlink.Link) (err error)
|
||||||
|
LinkSetMTU(link netlink.Link, mtu int) (err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type DNSLoop interface {
|
type DNSLoop interface {
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
tunnelUpData := tunnelUpData{
|
tunnelUpData := tunnelUpData{
|
||||||
|
vpnType: settings.Type,
|
||||||
serverIP: connection.IP,
|
serverIP: connection.IP,
|
||||||
serverName: connection.ServerName,
|
serverName: connection.ServerName,
|
||||||
canPortForward: connection.PortForward,
|
canPortForward: connection.PortForward,
|
||||||
@@ -56,14 +57,14 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
|||||||
password: settings.Provider.PortForwarding.Password,
|
password: settings.Provider.PortForwarding.Password,
|
||||||
}
|
}
|
||||||
|
|
||||||
openvpnCtx, openvpnCancel := context.WithCancel(context.Background())
|
vpnCtx, vpnCancel := context.WithCancel(context.Background())
|
||||||
waitError := make(chan error)
|
waitError := make(chan error)
|
||||||
tunnelReady := make(chan struct{})
|
tunnelReady := make(chan struct{})
|
||||||
|
|
||||||
go vpnRunner.Run(openvpnCtx, waitError, tunnelReady)
|
go vpnRunner.Run(vpnCtx, waitError, tunnelReady)
|
||||||
|
|
||||||
if err := l.waitForError(ctx, waitError); err != nil {
|
if err := l.waitForError(ctx, waitError); err != nil {
|
||||||
openvpnCancel()
|
vpnCancel()
|
||||||
l.crashed(ctx, err)
|
l.crashed(ctx, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -75,10 +76,10 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
|||||||
for stayHere {
|
for stayHere {
|
||||||
select {
|
select {
|
||||||
case <-tunnelReady:
|
case <-tunnelReady:
|
||||||
go l.onTunnelUp(openvpnCtx, ctx, tunnelUpData)
|
go l.onTunnelUp(vpnCtx, ctx, tunnelUpData)
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
l.cleanup()
|
l.cleanup()
|
||||||
openvpnCancel()
|
vpnCancel()
|
||||||
<-waitError
|
<-waitError
|
||||||
close(waitError)
|
close(waitError)
|
||||||
return
|
return
|
||||||
@@ -86,7 +87,7 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
|||||||
l.userTrigger = true
|
l.userTrigger = true
|
||||||
l.logger.Info("stopping")
|
l.logger.Info("stopping")
|
||||||
l.cleanup()
|
l.cleanup()
|
||||||
openvpnCancel()
|
vpnCancel()
|
||||||
<-waitError
|
<-waitError
|
||||||
// do not close waitError or the waitError
|
// do not close waitError or the waitError
|
||||||
// select case will trigger
|
// select case will trigger
|
||||||
@@ -99,7 +100,7 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
|||||||
l.statusManager.Lock() // prevent SetStatus from running in parallel
|
l.statusManager.Lock() // prevent SetStatus from running in parallel
|
||||||
|
|
||||||
l.cleanup()
|
l.cleanup()
|
||||||
openvpnCancel()
|
vpnCancel()
|
||||||
l.statusManager.SetStatus(constants.Crashed)
|
l.statusManager.SetStatus(constants.Crashed)
|
||||||
l.logAndWait(ctx, err)
|
l.logAndWait(ctx, err)
|
||||||
stayHere = false
|
stayHere = false
|
||||||
@@ -107,6 +108,6 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
|||||||
l.statusManager.Unlock()
|
l.statusManager.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
openvpnCancel()
|
vpnCancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,24 @@ package vpn
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/qdm12/dns/v2/pkg/check"
|
"github.com/qdm12/dns/v2/pkg/check"
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
|
"github.com/qdm12/gluetun/internal/pmtud"
|
||||||
"github.com/qdm12/gluetun/internal/version"
|
"github.com/qdm12/gluetun/internal/version"
|
||||||
|
"github.com/qdm12/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type tunnelUpData struct {
|
type tunnelUpData struct {
|
||||||
// Healthcheck
|
// Healthcheck
|
||||||
serverIP netip.Addr
|
serverIP netip.Addr
|
||||||
|
// vpnType is used for path MTU discovery to find the protocol overhead.
|
||||||
|
// It can be "wireguard" or "openvpn".
|
||||||
|
vpnType string
|
||||||
// Port forwarding
|
// Port forwarding
|
||||||
vpnIntf string
|
vpnIntf string
|
||||||
serverName string // used for PIA
|
serverName string // used for PIA
|
||||||
@@ -46,7 +54,14 @@ func (l *Loop) onTunnelUp(ctx, loopCtx context.Context, data tunnelUpData) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if *l.dnsLooper.GetSettings().DoT.Enabled {
|
mtuLogger := l.logger.New(log.SetComponent("MTU discovery"))
|
||||||
|
err = updateToMaxMTU(ctx, data.vpnIntf, data.vpnType,
|
||||||
|
l.netLinker, l.routing, mtuLogger)
|
||||||
|
if err != nil {
|
||||||
|
mtuLogger.Error(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if *l.dnsLooper.GetSettings().ServerEnabled {
|
||||||
_, _ = l.dnsLooper.ApplyStatus(ctx, constants.Running)
|
_, _ = l.dnsLooper.ApplyStatus(ctx, constants.Running)
|
||||||
} else {
|
} else {
|
||||||
err := check.WaitForDNS(ctx, check.Settings{})
|
err := check.WaitForDNS(ctx, check.Settings{})
|
||||||
@@ -112,3 +127,65 @@ func (l *Loop) restartVPN(ctx context.Context, healthErr error) {
|
|||||||
_, _ = l.ApplyStatus(ctx, constants.Stopped)
|
_, _ = l.ApplyStatus(ctx, constants.Stopped)
|
||||||
_, _ = l.ApplyStatus(ctx, constants.Running)
|
_, _ = l.ApplyStatus(ctx, constants.Running)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var errVPNTypeUnknown = errors.New("unknown VPN type")
|
||||||
|
|
||||||
|
func updateToMaxMTU(ctx context.Context, vpnInterface string,
|
||||||
|
vpnType string, netlinker NetLinker, routing Routing, logger *log.Logger,
|
||||||
|
) error {
|
||||||
|
logger.Info("finding maximum MTU, this can take up to 4 seconds")
|
||||||
|
|
||||||
|
vpnGatewayIP, err := routing.VPNLocalGatewayIP(vpnInterface)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting VPN gateway IP address: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
link, err := netlinker.LinkByName(vpnInterface)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting VPN interface by name: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
originalMTU := link.MTU
|
||||||
|
|
||||||
|
// Note: no point testing for an MTU of 1500, it will never work due to the VPN
|
||||||
|
// protocol overhead, so start lower than 1500 according to the protocol used.
|
||||||
|
const physicalLinkMTU = 1500
|
||||||
|
vpnLinkMTU := physicalLinkMTU
|
||||||
|
switch vpnType {
|
||||||
|
case "wireguard":
|
||||||
|
vpnLinkMTU -= 60 // Wireguard overhead
|
||||||
|
case "openvpn":
|
||||||
|
vpnLinkMTU -= 41 // OpenVPN overhead
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("%w: %q", errVPNTypeUnknown, vpnType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setting the VPN link MTU to 1500 might interrupt the connection until
|
||||||
|
// the new MTU is set again, but this is necessary to find the highest valid MTU.
|
||||||
|
logger.Debugf("VPN interface %s MTU temporarily set to %d", vpnInterface, vpnLinkMTU)
|
||||||
|
|
||||||
|
err = netlinker.LinkSetMTU(link, vpnLinkMTU)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("setting VPN interface %s MTU to %d: %w", vpnInterface, vpnLinkMTU, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pingTimeout = time.Second
|
||||||
|
vpnLinkMTU, err = pmtud.PathMTUDiscover(ctx, vpnGatewayIP, vpnLinkMTU, pingTimeout, logger)
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
logger.Infof("setting VPN interface %s MTU to maximum valid MTU %d", vpnInterface, vpnLinkMTU)
|
||||||
|
case errors.Is(err, pmtud.ErrMTUNotFound) || errors.Is(err, pmtud.ErrICMPNotPermitted):
|
||||||
|
vpnLinkMTU = int(originalMTU)
|
||||||
|
logger.Infof("reverting VPN interface %s MTU to %d (due to: %s)",
|
||||||
|
vpnInterface, originalMTU, err)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("path MTU discovering: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = netlinker.LinkSetMTU(link, vpnLinkMTU)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("setting VPN interface %s MTU to %d: %w", vpnInterface, vpnLinkMTU, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# Maintenance
|
# Maintenance
|
||||||
|
|
||||||
- Rename `UNBLOCK` to `DNS_HOSTNAMES_UNBLOCKED`
|
|
||||||
- Change `Run` methods to `Start`+`Stop`, returning channels rather than injecting them
|
- Change `Run` methods to `Start`+`Stop`, returning channels rather than injecting them
|
||||||
- Go 1.18
|
- Go 1.18
|
||||||
- gofumpt
|
- gofumpt
|
||||||
|
|||||||
Reference in New Issue
Block a user