Compare commits

...

23 Commits

Author SHA1 Message Date
Quentin McGaw
be935e70e6 Fix lint error 2025-11-07 21:54:30 +00:00
Quentin McGaw
5ca13021e7 feat(netlink): detect IPv6 using query to address
- If a default IPv6 route is found, query the ip:port defined by `IPV6_CHECK_ADDRESS` to check for internet access
2025-11-07 21:50:58 +00:00
Quentin McGaw
dae44051f6 feat(netlink): detect ipv6 support level
- 'supported' if one ipv6 route is found that is not loopback and not a default route
- 'internet' if one default ipv6 route is found
2025-11-07 21:50:58 +00:00
Quentin McGaw
ffb0bec4da chore(vpn): rename openvpn* to vpn* variables 2025-11-07 15:26:24 +00:00
Quentin McGaw
4d2b8787e0 chore(dns): replace UNBLOCK with DNS_UNBLOCK_HOSTNAMES 2025-11-07 14:36:10 +00:00
Quentin McGaw
d4831ad4a6 chore(dns): replace DOT_PRIVATE_ADDRESS with DNS_BLOCK_IPS and DNS_BLOCK_IP_PREFIXES 2025-11-07 14:31:09 +00:00
Quentin McGaw
9e1b53a732 feat(server): log number of roles read from auth file 2025-11-05 23:05:10 +00:00
Quentin McGaw
d0113849d6 feat(dns): support doh upstream type 2025-11-05 21:21:16 +00:00
Quentin McGaw
7b25fdfee8 chore(deps): bump dns to v2.0.0-rc9 2025-11-05 20:56:37 +00:00
Quentin McGaw
5ed6e82922 feat(dns): DNS_UPSTREAM_RESOLVER_TYPE option which can be plain or DoT
- Migrate `DOT` to `DNS_SERVER`
- Migrate `DOT_PROVIDERS` to `DNS_UPSTREAM_RESOLVERS`
- Migrate `DOT_PRIVATE_ADDRESS` to `DNS_PRIVATE_ADDRESSES`
- Migrate `DOT_CACHING` to `DNS_CACHING`
- Migrate `DOT_IPV6` to `DNS_UPSTREAM_IPV6`
2025-11-05 20:47:21 +00:00
Quentin McGaw
7dbd14df27 chore(dns): merge DoT settings with DNS settings 2025-11-05 20:47:21 +00:00
dependabot[bot]
96d8b53338 Chore(deps): Bump github.com/breml/rootcerts from 0.3.2 to 0.3.3 (#2964) 2025-11-04 20:34:22 -05:00
Quentin McGaw
2bd19640d9 feat(health/dns): try another DNS server if one fails 2025-11-04 15:51:04 +00:00
Quentin McGaw
1047508bd7 docs(github): update provider issue template 2025-11-04 15:07:16 +00:00
Quentin McGaw
eb49306b80 hotfix(health): change default icmp target to 1.1.1.1
- Cloudflare's 1.1.1.1 seems more reliable than the VPN server public IP address you connect to
- This can still be changed back to 0.0.0.0 to use the VPN server IP address if needed
2025-11-04 14:47:24 +00:00
Quentin McGaw
43da9ddbb3 fix(cyberghost): log warnings from updater resolver 2025-11-04 14:43:02 +00:00
Quentin McGaw
7fbc5c3c07 feat(cyberghost): update servers data 2025-11-04 14:43:02 +00:00
dependabot[bot]
e03f545e07 Chore(deps): Bump github.com/stretchr/testify from 1.10.0 to 1.11.1 (#2959) 2025-11-04 15:33:12 +01:00
dependabot[bot]
942f1f2c0f Chore(deps): Bump github.com/pelletier/go-toml/v2 from 2.2.3 to 2.2.4 (#2958) 2025-11-04 15:33:00 +01:00
dependabot[bot]
baf566d7a5 Chore(deps): Bump github.com/klauspost/compress from 1.17.11 to 1.18.1 (#2957) 2025-11-04 15:32:46 +01:00
Quentin McGaw
6712adfe6b hotfix(firewall): handle textual values for protocols
- Alpine / iptables-legacy bug introduced in Alpine 3.22
- Alpine: what the hell? Stop introducing breaking changes in iptables on every god damn release!
2025-11-04 14:16:11 +00:00
Quentin McGaw
2e2e5f9df5 fix(firewall): parse "all" protocol from iptables chains 2025-11-03 16:09:24 +00:00
Quentin McGaw
35e9b2365d fix(ci): consider 429 as valid status code for markdown links 2025-11-03 16:00:42 +00:00
45 changed files with 2049 additions and 2261 deletions

View File

@@ -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
- Publicly accessible URL to a structured (JSON etc.) list of servers **and attach** an example Openvpn configuration file for both TCP and UDP
- There is no need to support both OpenVPN and Wireguard for a provider, but it's better to support both if possible
- 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
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.

View File

@@ -8,6 +8,7 @@
"retryOn429": false,
"fallbackRetryDelay": "30s",
"aliveStatusCodes": [
200
200,
429
]
}

View File

@@ -159,23 +159,27 @@ ENV VPN_SERVICE_PROVIDER=pia \
FIREWALL_INPUT_PORTS= \
FIREWALL_OUTBOUND_SUBNETS= \
FIREWALL_DEBUG=off \
# IPv6
IPV6_CHECK_ADDRESS=[2606:4700::6810:84e5]:443 \
# Logging
LOG_LEVEL=info \
# Health
HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \
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 \
# DNS over TLS
DOT=on \
DOT_PROVIDERS=cloudflare \
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 \
DOT_CACHING=on \
DOT_IPV6=off \
# DNS
DNS_SERVER=on \
DNS_UPSTREAM_RESOLVER_TYPE=DoT \
DNS_UPSTREAM_RESOLVERS=cloudflare \
DNS_BLOCK_IPS= \
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_SURVEILLANCE=off \
BLOCK_ADS=off \
UNBLOCK= \
DNS_UNBLOCK_HOSTNAMES= \
DNS_UPDATE_PERIOD=24h \
DNS_ADDRESS=127.0.0.1 \
DNS_KEEP_NAMESERVER=off \

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"io/fs"
"net/http"
"net/netip"
"os"
"os/exec"
"os/signal"
@@ -242,10 +243,13 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
return err
}
ipv6Supported, err := netLinker.IsIPv6Supported()
ipv6SupportLevel, err := netLinker.FindIPv6SupportLevel(ctx,
allSettings.IPv6.CheckAddress, firewallConf)
if err != nil {
return fmt.Errorf("checking for IPv6 support: %w", err)
}
ipv6Supported := ipv6SupportLevel == netlink.IPv6Supported ||
ipv6SupportLevel == netlink.IPv6Internet
err = allSettings.Validate(storage, ipv6Supported, logger)
if err != nil {
@@ -430,7 +434,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(), openvpnFileExtractor)
vpnLogger := logger.New(log.SetComponent("vpn"))
vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6Supported, allSettings.Firewall.VPNInputPorts,
vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6SupportLevel, allSettings.Firewall.VPNInputPorts,
providers, storage, allSettings.Health, healthChecker, healthcheckServer, ovpnConf, netLinker, firewallConf,
routingConf, portForwardLooper, cmder, publicIPLooper, dnsLooper, vpnLogger, httpClient,
buildInfo, *allSettings.Version.Enabled)
@@ -474,7 +478,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
logger.New(log.SetComponent("http server")),
allSettings.ControlServer.AuthFilePath,
buildInfo, vpnLooper, portForwardLooper, dnsLooper, updaterLooper, publicIPLooper,
storage, ipv6Supported)
storage, ipv6SupportLevel.IsSupported())
if err != nil {
return fmt.Errorf("setting up control server: %w", err)
}
@@ -550,7 +554,9 @@ type netLinker interface {
Ruler
Linker
IsWireguardSupported() (ok bool, err error)
IsIPv6Supported() (ok bool, err error)
FindIPv6SupportLevel(ctx context.Context,
checkAddress netip.AddrPort, firewall netlink.Firewall,
) (level netlink.IPv6SupportLevel, err error)
PatchLoggerLevel(level log.Level)
}

12
go.mod
View File

@@ -3,20 +3,20 @@ module github.com/qdm12/gluetun
go 1.25.0
require (
github.com/breml/rootcerts v0.3.2
github.com/breml/rootcerts v0.3.3
github.com/fatih/color v1.18.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/pelletier/go-toml/v2 v2.2.3
github.com/qdm12/dns/v2 v2.0.0-rc8
github.com/pelletier/go-toml/v2 v2.2.4
github.com/qdm12/dns/v2 v2.0.0-rc9
github.com/qdm12/gosettings v0.4.4
github.com/qdm12/goshutdown v0.3.0
github.com/qdm12/gosplash v0.2.0
github.com/qdm12/gotree v0.3.0
github.com/qdm12/log v0.1.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/vishvananda/netlink v1.3.1
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/common v0.60.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/vishvananda/netns v0.0.5 // indirect
golang.org/x/crypto v0.43.0 // indirect

24
go.sum
View File

@@ -1,7 +1,7 @@
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/breml/rootcerts v0.3.2 h1:gm11iClhK8wFn/GDdINoDaqPkiaGXyVvSwoXINZN+z4=
github.com/breml/rootcerts v0.3.2/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
github.com/breml/rootcerts v0.3.3 h1://GnaRtQ/9BY2+GtMk2wtWxVdCRysiaPr5/xBwl7NKw=
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/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
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.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
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/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
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/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/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
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-rc8/go.mod h1:VaF02KWEL7xNV4oKfG4N9nEv/kR6bqyIcBReCV5NJhw=
github.com/qdm12/goservices v0.1.0 h1:9sODefm/yuIGS7ynCkEnNlMTAYn9GzPhtcK4F69JWvc=
github.com/qdm12/goservices v0.1.0/go.mod h1:/JOFsAnHFiSjyoXxa5FlfX903h20K5u/3rLzCjYVMck=
github.com/qdm12/dns/v2 v2.0.0-rc9 h1:qDzRkHr6993jknNB/ZOCnZOyIG6bsZcl2MIfdeUd0kI=
github.com/qdm12/dns/v2 v2.0.0-rc9/go.mod h1:98foWgXJZ+g8gJIuO+fdO+oWpFei5WShMFTeN4Im2lE=
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 h1:TRGpCU1l0lNwtogEUSs5U+RFceYxkAJUmrGabno7J5c=
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/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
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/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/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
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/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=

View File

@@ -0,0 +1,14 @@
package cli
import (
"context"
"net/netip"
)
type noopFirewall struct{}
func (f *noopFirewall) AcceptOutput(_ context.Context, _, _ string, _ netip.Addr,
_ uint16, _ bool,
) (err error) {
return nil
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/netlink"
"github.com/qdm12/gluetun/internal/openvpn/extract"
"github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/storage"
@@ -40,7 +41,9 @@ type IPFetcher interface {
}
type IPv6Checker interface {
IsIPv6Supported() (supported bool, err error)
FindIPv6SupportLevel(ctx context.Context,
checkAddress netip.AddrPort, firewall netlink.Firewall,
) (level netlink.IPv6SupportLevel, err error)
}
func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
@@ -58,12 +61,14 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
}
allSettings.SetDefaults()
ipv6Supported, err := ipv6Checker.IsIPv6Supported()
ipv6SupportLevel, err := ipv6Checker.FindIPv6SupportLevel(context.Background(),
allSettings.IPv6.CheckAddress, &noopFirewall{})
if err != nil {
return fmt.Errorf("checking for IPv6 support: %w", err)
}
if err = allSettings.Validate(storage, ipv6Supported, logger); err != nil {
err = allSettings.Validate(storage, ipv6SupportLevel.IsSupported(), logger)
if err != nil {
return fmt.Errorf("validating settings: %w", err)
}
@@ -79,13 +84,13 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor)
providerConf := providers.Get(allSettings.VPN.Provider.Name)
connection, err := providerConf.GetConnection(
allSettings.VPN.Provider.ServerSelection, ipv6Supported)
allSettings.VPN.Provider.ServerSelection, ipv6SupportLevel == netlink.IPv6Internet)
if err != nil {
return err
}
lines := providerConf.OpenVPNConfig(connection,
allSettings.VPN.OpenVPN, ipv6Supported)
allSettings.VPN.OpenVPN, ipv6SupportLevel.IsSupported())
fmt.Println(strings.Join(lines, "\n"))
return nil

View File

@@ -1,9 +1,13 @@
package settings
import (
"errors"
"fmt"
"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/reader"
"github.com/qdm12/gotree"
@@ -11,10 +15,31 @@ import (
// DNS contains settings to configure DNS.
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
// the Go program and for the system.
// 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.
ServerAddress netip.Addr
// KeepNameserver is true if the existing DNS server
@@ -23,20 +48,40 @@ type DNS struct {
// outside the VPN tunnel since it would go through
// the local DNS server of your Docker/Kubernetes
// 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.
// It defaults to false and cannot be nil in the
// internal state.
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) {
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 {
return fmt.Errorf("validating DoT settings: %w", err)
return err
}
return nil
@@ -44,9 +89,15 @@ func (d DNS) validate() (err error) {
func (d *DNS) Copy() (copied 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,
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.
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.KeepNameserver = gosettings.OverrideWithPointer(d.KeepNameserver, other.KeepNameserver)
d.DoT.overrideWith(other.DoT)
}
func (d *DNS) setDefaults() {
localhost := netip.AddrFrom4([4]byte{127, 0, 0, 1})
d.ServerAddress = gosettings.DefaultValidator(d.ServerAddress, localhost)
d.ServerEnabled = gosettings.DefaultPointer(d.ServerEnabled, true)
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.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 {
@@ -77,11 +160,63 @@ func (d DNS) toLinesNode() (node *gotree.Node) {
return node
}
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
}
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"))
if err != nil {
return err
@@ -92,10 +227,5 @@ func (d *DNS) read(r *reader.Reader) (err error) {
return err
}
err = d.DoT.read(r)
if err != nil {
return fmt.Errorf("DNS over TLS settings: %w", err)
}
return nil
}

View File

@@ -149,23 +149,45 @@ func (b *DNSBlacklist) read(r *reader.Reader) (err error) {
return err
}
b.AddBlockedIPs, b.AddBlockedIPPrefixes,
err = readDoTPrivateAddresses(r) // TODO v4 split in 2
b.AddBlockedIPs, b.AddBlockedIPPrefixes, err = readDNSBlockedIPs(r)
if err != nil {
return err
}
b.AllowedHosts = r.CSV("UNBLOCK") // TODO v4 change name
b.AllowedHosts = r.CSV("DNS_UNBLOCK_HOSTNAMES", reader.RetroKeys("UNBLOCK"))
return nil
}
var ErrPrivateAddressNotValid = errors.New("private address is not a valid IP or CIDR range")
func readDoTPrivateAddresses(reader *reader.Reader) (ips []netip.Addr,
func readDNSBlockedIPs(r *reader.Reader) (ips []netip.Addr,
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 {
return nil, nil, nil
}

View File

@@ -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
}

View File

@@ -29,7 +29,7 @@ type Health struct {
// It cannot be the empty string in the internal state.
TargetAddress string
// 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.
ICMPTargetIP netip.Addr
// RestartVPN indicates whether to restart the VPN connection

View File

@@ -0,0 +1,51 @@
package settings
import (
"net/netip"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
)
// IPv6 contains settings regarding IPv6 configuration.
type IPv6 struct {
// CheckAddress is the TCP ip:port address to dial to check
// IPv6 is supported, in case a default IPv6 route is found.
// It defaults to cloudflare.com address [2606:4700::6810:84e5]:443
CheckAddress netip.AddrPort
}
func (i IPv6) validate() (err error) {
return nil
}
func (i *IPv6) copy() (copied IPv6) {
return IPv6{
CheckAddress: i.CheckAddress,
}
}
func (i *IPv6) overrideWith(other IPv6) {
i.CheckAddress = gosettings.OverrideWithValidator(i.CheckAddress, other.CheckAddress)
}
func (i *IPv6) setDefaults() {
defaultCheckAddress := netip.MustParseAddrPort("[2606:4700::6810:84e5]:443")
i.CheckAddress = gosettings.DefaultComparable(i.CheckAddress, defaultCheckAddress)
}
func (i IPv6) String() string {
return i.toLinesNode().String()
}
func (i IPv6) toLinesNode() (node *gotree.Node) {
node = gotree.New("IPv6 settings:")
node.Appendf("Check address: %s", i.CheckAddress)
return node
}
func (i *IPv6) read(r *reader.Reader) (err error) {
i.CheckAddress, err = r.NetipAddrPort("IPV6_CHECK_ADDRESS")
return err
}

View File

@@ -27,6 +27,7 @@ type Settings struct {
Updater Updater
Version Version
VPN VPN
IPv6 IPv6
Pprof pprof.Settings
}
@@ -53,6 +54,7 @@ func (s *Settings) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Support
"system": s.System.validate,
"updater": s.Updater.Validate,
"version": s.Version.validate,
"ipv6": s.IPv6.validate,
// Pprof validation done in pprof constructor
"VPN": func() error {
return s.VPN.Validate(filterChoicesGetter, ipv6Supported, warner)
@@ -85,6 +87,7 @@ func (s *Settings) copy() (copied Settings) {
Version: s.Version.copy(),
VPN: s.VPN.Copy(),
Pprof: s.Pprof.Copy(),
IPv6: s.IPv6.copy(),
}
}
@@ -106,6 +109,7 @@ func (s *Settings) OverrideWith(other Settings,
patchedSettings.Version.overrideWith(other.Version)
patchedSettings.VPN.OverrideWith(other.VPN)
patchedSettings.Pprof.OverrideWith(other.Pprof)
patchedSettings.IPv6.overrideWith(other.IPv6)
err = patchedSettings.Validate(filterChoicesGetter, ipv6Supported, warner)
if err != nil {
return err
@@ -121,6 +125,7 @@ func (s *Settings) SetDefaults() {
s.Health.SetDefaults()
s.HTTPProxy.setDefaults()
s.Log.setDefaults()
s.IPv6.setDefaults()
s.PublicIP.setDefaults()
s.Shadowsocks.setDefaults()
s.Storage.setDefaults()
@@ -142,6 +147,7 @@ func (s Settings) toLinesNode() (node *gotree.Node) {
node.AppendNode(s.DNS.toLinesNode())
node.AppendNode(s.Firewall.toLinesNode())
node.AppendNode(s.Log.toLinesNode())
node.AppendNode(s.IPv6.toLinesNode())
node.AppendNode(s.Health.toLinesNode())
node.AppendNode(s.Shadowsocks.toLinesNode())
node.AppendNode(s.HTTPProxy.toLinesNode())
@@ -177,10 +183,10 @@ func (s Settings) Warnings() (warnings []string) {
// TODO remove in v4
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()+
" so the DNS over TLS (DoT) server will not be used."+
" The default value changed to 127.0.0.1 so it uses the internal DoT serves."+
" If the DoT server fails to start, the IPv4 address of the first plaintext DNS server"+
" corresponding to the first DoT provider chosen is 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 DNS server."+
" If this server fails to start, the IPv4 address of the first plaintext DNS server"+
" corresponding to the first DNS provider chosen is used.")
}
return warnings
@@ -208,6 +214,7 @@ func (s *Settings) Read(r *reader.Reader, warner Warner) (err error) {
"updater": s.Updater.read,
"version": s.Version.read,
"VPN": s.VPN.read,
"IPv6": s.IPv6.read,
"profiling": s.Pprof.Read,
}

View File

@@ -40,21 +40,23 @@ func Test_Settings_String(t *testing.T) {
├── DNS settings:
| ├── Keep existing nameserver(s): no
| ├── DNS server address to use: 127.0.0.1
| ── DNS over TLS settings:
| ├── Enabled: yes
| ├── Update period: every 24h0m0s
| ── Upstream resolvers:
| | └── Cloudflare
| ├── Caching: yes
| ├── IPv6: no
| └── DNS filtering settings:
| ├── Block malicious: yes
| ├── Block ads: no
| └── Block surveillance: yes
| ── DNS forwarder server enabled: yes
| ├── Upstream resolver type: dot
| ├── Upstream resolvers:
| | ── Cloudflare
| ├── Caching: yes
| ├── IPv6: no
| ├── Update period: every 24h0m0s
| └── DNS filtering settings:
| ├── Block malicious: yes
| ├── Block ads: no
| └── Block surveillance: yes
├── Firewall settings:
| └── Enabled: yes
├── Log settings:
| └── Log level: INFO
├── IPv6 settings:
| └── Check address: [2606:4700::6810:84e5]:443
├── Health settings:
| ├── Server listening address: 127.0.0.1:9999
| ├── Target address: cloudflare.com:443

View File

@@ -10,15 +10,7 @@ import (
func (l *Loop) useUnencryptedDNS(fallback bool) {
settings := l.GetSettings()
// Try with user provided plaintext ip address
// 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()
}
targetIP := settings.GetFirstPlaintextIPv4()
if fallback {
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 defaultDNSPort = 53
settingsInternalDNS := nameserver.SettingsInternalDNS{
IP: targetIP,
Timeout: dialTimeout,
AddrPort: netip.AddrPortFrom(targetIP, defaultDNSPort),
Timeout: dialTimeout,
}
nameserver.UseDNSInternally(settingsInternalDNS)
settingsSystemWide := nameserver.SettingsSystemDNS{
IP: targetIP,
IPs: []netip.Addr{targetIP},
ResolvPath: l.resolvConf,
}
err := nameserver.UseDNSSystemWide(settingsSystemWide)

View File

@@ -26,12 +26,12 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
}
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
var runError <-chan error
settings := l.GetSettings()
for !*settings.KeepNameserver && *settings.DoT.Enabled {
for !*settings.KeepNameserver && *settings.ServerEnabled {
var err error
runError, err = l.setupServer(ctx)
if err == nil {
@@ -56,7 +56,7 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
}
settings = l.GetSettings()
if !*settings.KeepNameserver && !*settings.DoT.Enabled {
if !*settings.KeepNameserver && !*settings.ServerEnabled {
const fallback = false
l.useUnencryptedDNS(fallback)
}
@@ -101,6 +101,6 @@ func (l *Loop) runWait(ctx context.Context, runError <-chan error) (exitLoop boo
func (l *Loop) stopServer() {
stopErr := l.server.Stop()
if stopErr != nil {
l.logger.Error("stopping DoT server: " + stopErr.Error())
l.logger.Error("stopping server: " + stopErr.Error())
}
}

View File

@@ -4,11 +4,13 @@ import (
"context"
"fmt"
"github.com/qdm12/dns/v2/pkg/doh"
"github.com/qdm12/dns/v2/pkg/dot"
cachemiddleware "github.com/qdm12/dns/v2/pkg/middlewares/cache"
"github.com/qdm12/dns/v2/pkg/middlewares/cache/lru"
filtermiddleware "github.com/qdm12/dns/v2/pkg/middlewares/filter"
"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/server"
"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)
}
func buildDoTSettings(settings settings.DNS,
func buildServerSettings(settings settings.DNS,
filter *mapfilter.Filter, logger Logger) (
serverSettings server.Settings, err error,
) {
serverSettings.Logger = logger
var dotSettings dot.Settings
providersData := provider.NewProviders()
dotSettings.UpstreamResolvers = make([]provider.Provider, len(settings.DoT.Providers))
for i := range settings.DoT.Providers {
upstreamResolvers := make([]provider.Provider, len(settings.Providers))
for i := range settings.Providers {
var err error
dotSettings.UpstreamResolvers[i], err = providersData.Get(settings.DoT.Providers[i])
upstreamResolvers[i], err = providersData.Get(settings.Providers[i])
if err != nil {
panic(err) // this should already had been checked
}
}
dotSettings.IPVersion = "ipv4"
if *settings.DoT.IPv6 {
dotSettings.IPVersion = "ipv6"
ipVersion := "ipv4"
if *settings.IPv6 {
ipVersion = "ipv6"
}
serverSettings.Dialer, err = dot.New(dotSettings)
if err != nil {
return server.Settings{}, fmt.Errorf("creating DNS over TLS dialer: %w", err)
var dialer server.Dialer
switch settings.UpstreamType {
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{})
if err != nil {
return server.Settings{}, fmt.Errorf("creating LRU cache: %w", err)

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"net/netip"
"github.com/qdm12/dns/v2/pkg/check"
"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()
dotSettings, err := buildDoTSettings(settings, l.filter, l.logger)
serverSettings, err := buildServerSettings(settings, l.filter, l.logger)
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 {
return nil, fmt.Errorf("creating DoT server: %w", err)
return nil, fmt.Errorf("creating server: %w", err)
}
runError, err = server.Start(ctx)
@@ -37,11 +38,12 @@ func (l *Loop) setupServer(ctx context.Context) (runError <-chan error, err erro
l.server = server
// use internal DNS server
const defaultDNSPort = 53
nameserver.UseDNSInternally(nameserver.SettingsInternalDNS{
IP: settings.ServerAddress,
AddrPort: netip.AddrPortFrom(settings.ServerAddress, defaultDNSPort),
})
err = nameserver.UseDNSSystemWide(nameserver.SettingsSystemDNS{
IP: settings.ServerAddress,
IPs: []netip.Addr{settings.ServerAddress},
ResolvPath: l.resolvConf,
})
if err != nil {

View File

@@ -27,7 +27,7 @@ func (s *State) SetSettings(ctx context.Context, settings settings.DNS) (
// Check for only update period change
tempSettings := s.settings.Copy()
*tempSettings.DoT.UpdatePeriod = *settings.DoT.UpdatePeriod
*tempSettings.UpdatePeriod = *settings.UpdatePeriod
onlyUpdatePeriodChanged := reflect.DeepEqual(tempSettings, settings)
s.settings = settings
@@ -40,7 +40,7 @@ func (s *State) SetSettings(ctx context.Context, settings settings.DNS) (
// Restart
_, _ = s.statusApplier.ApplyStatus(ctx, constants.Stopped)
if *settings.DoT.Enabled {
if *settings.ServerEnabled {
outcome, _ = s.statusApplier.ApplyStatus(ctx, constants.Running)
}
return outcome

View File

@@ -14,7 +14,7 @@ func (l *Loop) RunRestartTicker(ctx context.Context, done chan<- struct{}) {
timer.Stop()
timerIsStopped := true
settings := l.GetSettings()
if period := *settings.DoT.UpdatePeriod; period > 0 {
if period := *settings.UpdatePeriod; period > 0 {
timer.Reset(period)
timerIsStopped = false
}
@@ -43,14 +43,14 @@ func (l *Loop) RunRestartTicker(ctx context.Context, done chan<- struct{}) {
_, _ = l.statusManager.ApplyStatus(ctx, constants.Running)
settings := l.GetSettings()
timer.Reset(*settings.DoT.UpdatePeriod)
timer.Reset(*settings.UpdatePeriod)
case <-l.updateTicker:
if !timer.Stop() {
<-timer.C
}
timerIsStopped = true
settings := l.GetSettings()
newUpdatePeriod := *settings.DoT.UpdatePeriod
newUpdatePeriod := *settings.UpdatePeriod
if newUpdatePeriod == 0 {
continue
}

View File

@@ -12,7 +12,7 @@ func (l *Loop) updateFiles(ctx context.Context) (err error) {
settings := l.GetSettings()
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)
if err != nil {

View File

@@ -162,6 +162,24 @@ func (c *Config) acceptOutputTrafficToVPN(ctx context.Context,
return c.runIP6tablesInstruction(ctx, instruction)
}
func (c *Config) AcceptOutput(ctx context.Context,
protocol, intf string, ip netip.Addr, port uint16, remove bool,
) error {
interfaceFlag := "-o " + intf
if intf == "*" { // all interfaces
interfaceFlag = ""
}
instruction := fmt.Sprintf("%s OUTPUT -d %s %s -p %s -m %s --dport %d -j ACCEPT",
appendOrDelete(remove), ip, interfaceFlag, protocol, protocol, port)
if ip.Is4() {
return c.runIptablesInstruction(ctx, instruction)
} else if c.ip6Tables == "" {
return fmt.Errorf("accept output to VPN server: %w", ErrNeedIP6Tables)
}
return c.runIP6tablesInstruction(ctx, instruction)
}
// Thanks to @npawelek.
func (c *Config) acceptOutputFromIPToSubnet(ctx context.Context,
intf string, sourceIP netip.Addr, destinationSubnet netip.Prefix, remove bool,

View File

@@ -323,12 +323,12 @@ var ErrProtocolUnknown = errors.New("unknown protocol")
func parseProtocol(s string) (protocol string, err error) {
switch s {
case "0":
case "1":
case "0", "all":
case "1", "icmp":
protocol = "icmp"
case "6":
case "6", "tcp":
protocol = "tcp"
case "17":
case "17", "udp":
protocol = "udp"
default:
return "", fmt.Errorf("%w: %s", ErrProtocolUnknown, s)

View File

@@ -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
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
5 0 0 ACCEPT all -- tun0 * 1.2.3.4 0.0.0.0/0
`,
table: chain{
name: "INPUT",
@@ -111,6 +112,17 @@ num pkts bytes target prot opt in out source destinati
source: netip.MustParsePrefix("1.2.3.4/32"),
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"),
},
},
},
},

View File

@@ -5,33 +5,60 @@ import (
"errors"
"fmt"
"net"
"net/netip"
"github.com/qdm12/dns/v2/pkg/provider"
)
// 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,
// because we don't want to use DoT or DoH and impact the TCP connections
// when running a healthcheck.
type Client struct{}
type Client struct {
serverAddrs []netip.AddrPort
dnsIPIndex int
}
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")
func (c *Client) Check(ctx context.Context) error {
dnsAddr := c.serverAddrs[c.dnsIPIndex].Addr()
resolver := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, _, _ string) (net.Conn, error) {
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")
switch {
case err != nil:
c.dnsIPIndex = (c.dnsIPIndex + 1) % len(c.serverAddrs)
return err
case len(ips) == 0:
c.dnsIPIndex = (c.dnsIPIndex + 1) % len(c.serverAddrs)
return fmt.Errorf("%w", ErrLookupNoIPs)
default:
return nil

View File

@@ -1,9 +1,19 @@
package netlink
import "github.com/qdm12/log"
import (
"context"
"net/netip"
"github.com/qdm12/log"
)
type DebugLogger interface {
Debug(message string)
Debugf(format string, args ...any)
Patch(options ...log.Option)
}
type Firewall interface {
AcceptOutput(ctx context.Context, protocol, intf string, ip netip.Addr,
port uint16, remove bool) (err error)
}

View File

@@ -1,37 +1,106 @@
package netlink
import (
"context"
"fmt"
"net"
"net/netip"
"time"
)
func (n *NetLink) IsIPv6Supported() (supported bool, err error) {
type IPv6SupportLevel uint8
const (
IPv6Unsupported = iota
// IPv6Supported indicates the host supports IPv6 but has no access to the
// Internet via IPv6. It is true if one IPv6 route is found and no default
// IPv6 route is found.
IPv6Supported
// IPv6Internet indicates the host has access to the Internet via IPv6,
// which is detected when a default IPv6 route is found.
IPv6Internet
)
func (i IPv6SupportLevel) IsSupported() bool {
return i == IPv6Supported || i == IPv6Internet
}
func (n *NetLink) FindIPv6SupportLevel(ctx context.Context,
checkAddress netip.AddrPort, firewall Firewall,
) (level IPv6SupportLevel, err error) {
routes, err := n.RouteList(FamilyV6)
if err != nil {
return false, fmt.Errorf("listing IPv6 routes: %w", err)
return IPv6Unsupported, fmt.Errorf("listing IPv6 routes: %w", err)
}
// Check each route for IPv6 due to Podman bug listing IPv4 routes
// as IPv6 routes at container start, see:
// https://github.com/qdm12/gluetun/issues/1241#issuecomment-1333405949
level = IPv6Unsupported
for _, route := range routes {
link, err := n.LinkByIndex(route.LinkIndex)
if err != nil {
return false, fmt.Errorf("finding link corresponding to route: %w", err)
return IPv6Unsupported, fmt.Errorf("finding link corresponding to route: %w", err)
}
sourceIsIPv6 := route.Src.IsValid() && route.Src.Is6()
sourceIsIPv4 := route.Src.IsValid() && route.Src.Is4()
destinationIsIPv4 := route.Dst.IsValid() && route.Dst.Addr().Is4()
destinationIsIPv6 := route.Dst.IsValid() && route.Dst.Addr().Is6()
switch {
case !sourceIsIPv6 && !destinationIsIPv6,
case sourceIsIPv4 && destinationIsIPv4,
destinationIsIPv6 && route.Dst.Addr().IsLoopback():
continue
case route.Dst.Addr().IsUnspecified(): // default ipv6 route
n.debugLogger.Debugf("IPv6 default route found on link %s", link.Name)
err = dialAddrThroughFirewall(ctx, link.Name, checkAddress, firewall)
if err != nil {
n.debugLogger.Debugf("IPv6 query failed on %s: %w", link.Name, err)
level = IPv6Supported
continue
}
n.debugLogger.Debugf("IPv6 internet is accessible through link %s", link.Name)
return IPv6Internet, nil
default: // non-default ipv6 route found
n.debugLogger.Debugf("IPv6 is supported by link %s", link.Name)
level = IPv6Supported
}
n.debugLogger.Debugf("IPv6 is supported by link %s", link.Name)
return true, nil
}
n.debugLogger.Debugf("IPv6 is not supported after searching %d routes",
len(routes))
return false, nil
if level == IPv6Unsupported {
n.debugLogger.Debugf("no IPv6 route found in %d routes", len(routes))
}
return level, nil
}
func dialAddrThroughFirewall(ctx context.Context, intf string,
checkAddress netip.AddrPort, firewall Firewall,
) (err error) {
const protocol = "tcp"
remove := false
err = firewall.AcceptOutput(ctx, protocol, intf,
checkAddress.Addr(), checkAddress.Port(), remove)
if err != nil {
return fmt.Errorf("accepting output traffic: %w", err)
}
defer func() {
remove = true
firewallErr := firewall.AcceptOutput(ctx, protocol, intf,
checkAddress.Addr(), checkAddress.Port(), remove)
if err == nil && firewallErr != nil {
err = fmt.Errorf("removing output traffic rule: %w", firewallErr)
}
}()
dialer := &net.Dialer{
Timeout: time.Second,
}
conn, err := dialer.DialContext(ctx, protocol, checkAddress.String())
if err != nil {
return fmt.Errorf("dialing: %w", err)
}
err = conn.Close()
if err != nil {
return fmt.Errorf("closing connection: %w", err)
}
return nil
}

View File

@@ -0,0 +1,167 @@
package netlink
import (
"context"
"errors"
"net"
"net/netip"
"strings"
"testing"
"time"
gomock "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func isIPv6LocallySupported() bool {
dialer := net.Dialer{Timeout: time.Millisecond}
_, err := dialer.Dial("tcp6", "[::1]:9999")
return !strings.HasSuffix(err.Error(), "connect: cannot assign requested address")
}
// Susceptible to TOCTOU but it should be fine for the use case.
func findAvailableTCPPort(t *testing.T) (port uint16) {
t.Helper()
config := &net.ListenConfig{}
listener, err := config.Listen(context.Background(), "tcp", "localhost:0")
require.NoError(t, err)
addr := listener.Addr().String()
err = listener.Close()
require.NoError(t, err)
addrPort, err := netip.ParseAddrPort(addr)
require.NoError(t, err)
return addrPort.Port()
}
func Test_dialAddrThroughFirewall(t *testing.T) {
t.Parallel()
errTest := errors.New("test error")
const ipv6InternetWorks = false
testCases := map[string]struct {
getIPv6CheckAddr func(t *testing.T) netip.AddrPort
firewallAddErr error
firewallRemoveErr error
errMessageRegex func() string
}{
"cloudflare.com": {
getIPv6CheckAddr: func(_ *testing.T) netip.AddrPort {
return netip.MustParseAddrPort("[2606:4700::6810:84e5]:443")
},
errMessageRegex: func() string {
if ipv6InternetWorks {
return ""
}
return "dialing: dial tcp \\[2606:4700::6810:84e5\\]:443: " +
"connect: (cannot assign requested address|network is unreachable)"
},
},
"local_server": {
getIPv6CheckAddr: func(t *testing.T) netip.AddrPort {
t.Helper()
network := "tcp6"
loopback := netip.MustParseAddr("::1")
if !isIPv6LocallySupported() {
network = "tcp4"
loopback = netip.MustParseAddr("127.0.0.1")
}
listener, err := net.ListenTCP(network, nil)
require.NoError(t, err)
t.Cleanup(func() {
err := listener.Close()
require.NoError(t, err)
})
addrPort := netip.MustParseAddrPort(listener.Addr().String())
return netip.AddrPortFrom(loopback, addrPort.Port())
},
},
"no_local_server": {
getIPv6CheckAddr: func(t *testing.T) netip.AddrPort {
t.Helper()
loopback := netip.MustParseAddr("::1")
if !ipv6InternetWorks {
loopback = netip.MustParseAddr("127.0.0.1")
}
availablePort := findAvailableTCPPort(t)
return netip.AddrPortFrom(loopback, availablePort)
},
errMessageRegex: func() string {
return "dialing: dial tcp (\\[::1\\]|127\\.0\\.0\\.1):[1-9][0-9]{1,4}: " +
"connect: connection refused"
},
},
"firewall_add_error": {
firewallAddErr: errTest,
errMessageRegex: func() string {
return "accepting output traffic: test error"
},
},
"firewall_remove_error": {
getIPv6CheckAddr: func(t *testing.T) netip.AddrPort {
t.Helper()
network := "tcp4"
loopback := netip.MustParseAddr("127.0.0.1")
listener, err := net.ListenTCP(network, nil)
require.NoError(t, err)
t.Cleanup(func() {
err := listener.Close()
require.NoError(t, err)
})
addrPort := netip.MustParseAddrPort(listener.Addr().String())
return netip.AddrPortFrom(loopback, addrPort.Port())
},
firewallRemoveErr: errTest,
errMessageRegex: func() string {
return "removing output traffic rule: test error"
},
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
var checkAddr netip.AddrPort
if testCase.getIPv6CheckAddr != nil {
checkAddr = testCase.getIPv6CheckAddr(t)
}
ctx := context.Background()
const intf = "eth0"
firewall := NewMockFirewall(ctrl)
call := firewall.EXPECT().AcceptOutput(ctx, "tcp", intf,
checkAddr.Addr(), checkAddr.Port(), false).
Return(testCase.firewallAddErr)
if testCase.firewallAddErr == nil {
firewall.EXPECT().AcceptOutput(ctx, "tcp", intf,
checkAddr.Addr(), checkAddr.Port(), true).
Return(testCase.firewallRemoveErr).After(call)
}
err := dialAddrThroughFirewall(ctx, intf, checkAddr, firewall)
var errMessageRegex string
if testCase.errMessageRegex != nil {
errMessageRegex = testCase.errMessageRegex()
}
if errMessageRegex == "" {
assert.NoError(t, err)
} else {
require.Error(t, err)
assert.Regexp(t, errMessageRegex, err.Error())
}
})
}
}

View File

@@ -0,0 +1,3 @@
package netlink
//go:generate mockgen -destination=mocks_test.go -package=$GOPACKAGE . Firewall

View File

@@ -0,0 +1,50 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/qdm12/gluetun/internal/netlink (interfaces: Firewall)
// Package netlink is a generated GoMock package.
package netlink
import (
context "context"
netip "net/netip"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockFirewall is a mock of Firewall interface.
type MockFirewall struct {
ctrl *gomock.Controller
recorder *MockFirewallMockRecorder
}
// MockFirewallMockRecorder is the mock recorder for MockFirewall.
type MockFirewallMockRecorder struct {
mock *MockFirewall
}
// NewMockFirewall creates a new mock instance.
func NewMockFirewall(ctrl *gomock.Controller) *MockFirewall {
mock := &MockFirewall{ctrl: ctrl}
mock.recorder = &MockFirewallMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockFirewall) EXPECT() *MockFirewallMockRecorder {
return m.recorder
}
// AcceptOutput mocks base method.
func (m *MockFirewall) AcceptOutput(arg0 context.Context, arg1, arg2 string, arg3 netip.Addr, arg4 uint16, arg5 bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AcceptOutput", arg0, arg1, arg2, arg3, arg4, arg5)
ret0, _ := ret[0].(error)
return ret0
}
// AcceptOutput indicates an expected call of AcceptOutput.
func (mr *MockFirewallMockRecorder) AcceptOutput(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AcceptOutput", reflect.TypeOf((*MockFirewall)(nil).AcceptOutput), arg0, arg1, arg2, arg3, arg4, arg5)
}

View File

@@ -15,12 +15,12 @@ type Provider struct {
}
func New(storage common.Storage, randSource rand.Source,
parallelResolver common.ParallelResolver,
updaterWarner common.Warner, parallelResolver common.ParallelResolver,
) *Provider {
return &Provider{
storage: storage,
randSource: randSource,
Fetcher: updater.New(parallelResolver),
Fetcher: updater.New(parallelResolver, updaterWarner),
}
}

View File

@@ -16,7 +16,10 @@ func (u *Updater) FetchServers(ctx context.Context, minServers int) (
possibleHosts := possibleServers.hostsSlice()
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 {
return nil, err
}

View File

@@ -6,10 +6,12 @@ import (
type Updater struct {
parallelResolver common.ParallelResolver
warner common.Warner
}
func New(parallelResolver common.ParallelResolver) *Updater {
func New(parallelResolver common.ParallelResolver, warner common.Warner) *Updater {
return &Updater{
parallelResolver: parallelResolver,
warner: warner,
}
}

View File

@@ -62,7 +62,7 @@ func NewProviders(storage Storage, timeNow func() time.Time,
providerNameToProvider := map[string]Provider{
providers.Airvpn: airvpn.New(storage, randSource, client),
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.Fastestvpn: fastestvpn.New(storage, randSource, client, updaterWarner, parallelResolver),
providers.Giganews: giganews.New(storage, randSource, unzipper, updaterWarner, parallelResolver),

View File

@@ -15,6 +15,7 @@ type infoWarner interface {
type infoer interface {
Info(s string)
Infof(format string, args ...any)
}
type warner interface {

View File

@@ -13,9 +13,6 @@ import (
func Read(filepath string) (settings Settings, err error) {
file, err := os.Open(filepath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return Settings{}, nil
}
return settings, fmt.Errorf("opening file: %w", err)
}
decoder := toml.NewDecoder(file)

View File

@@ -2,7 +2,9 @@ package server
import (
"context"
"errors"
"fmt"
"os"
"github.com/qdm12/gluetun/internal/httpserver"
"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,
) {
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)
default:
logger.Infof("read %d roles from authentication file", len(authSettings.Roles))
}
authSettings.SetDefaults()
err = authSettings.Validate()

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,7 @@ import (
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/loopstate"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/netlink"
"github.com/qdm12/gluetun/internal/vpn/state"
"github.com/qdm12/log"
)
@@ -21,10 +22,10 @@ type Loop struct {
healthChecker HealthChecker
healthServer HealthServer
// Fixed parameters
buildInfo models.BuildInformation
versionInfo bool
ipv6Supported bool
vpnInputPorts []uint16 // TODO make changeable through stateful firewall
buildInfo models.BuildInformation
versionInfo bool
ipv6SupportLevel netlink.IPv6SupportLevel
vpnInputPorts []uint16 // TODO make changeable through stateful firewall
// Configurators
openvpnConf OpenVPN
netLinker NetLinker
@@ -51,7 +52,7 @@ const (
defaultBackoffTime = 15 * time.Second
)
func NewLoop(vpnSettings settings.VPN, ipv6Supported bool, vpnInputPorts []uint16,
func NewLoop(vpnSettings settings.VPN, ipv6SupportLevel netlink.IPv6SupportLevel, vpnInputPorts []uint16,
providers Providers, storage Storage, healthSettings settings.Health,
healthChecker HealthChecker, healthServer HealthServer, openvpnConf OpenVPN,
netLinker NetLinker, fw Firewall, routing Routing,
@@ -69,32 +70,32 @@ func NewLoop(vpnSettings settings.VPN, ipv6Supported bool, vpnInputPorts []uint1
state := state.New(statusManager, vpnSettings)
return &Loop{
statusManager: statusManager,
state: state,
providers: providers,
storage: storage,
healthSettings: healthSettings,
healthChecker: healthChecker,
healthServer: healthServer,
buildInfo: buildInfo,
versionInfo: versionInfo,
ipv6Supported: ipv6Supported,
vpnInputPorts: vpnInputPorts,
openvpnConf: openvpnConf,
netLinker: netLinker,
fw: fw,
routing: routing,
portForward: portForward,
publicip: publicip,
dnsLooper: dnsLooper,
starter: starter,
logger: logger,
client: client,
start: start,
running: running,
stop: stop,
stopped: stopped,
userTrigger: true,
backoffTime: defaultBackoffTime,
statusManager: statusManager,
state: state,
providers: providers,
storage: storage,
healthSettings: healthSettings,
healthChecker: healthChecker,
healthServer: healthServer,
buildInfo: buildInfo,
versionInfo: versionInfo,
ipv6SupportLevel: ipv6SupportLevel,
vpnInputPorts: vpnInputPorts,
openvpnConf: openvpnConf,
netLinker: netLinker,
fw: fw,
routing: routing,
portForward: portForward,
publicip: publicip,
dnsLooper: dnsLooper,
starter: starter,
logger: logger,
client: client,
start: start,
running: running,
stop: stop,
stopped: stopped,
userTrigger: true,
backoffTime: defaultBackoffTime,
}
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/netlink"
"github.com/qdm12/gluetun/internal/openvpn"
"github.com/qdm12/gluetun/internal/provider"
)
@@ -14,15 +15,16 @@ import (
// It returns a serverName for port forwarding (PIA) and an error if it fails.
func setupOpenVPN(ctx context.Context, fw Firewall,
openvpnConf OpenVPN, providerConf provider.Provider,
settings settings.VPN, ipv6Supported bool, starter CmdStarter,
settings settings.VPN, ipv6SupportLevel netlink.IPv6SupportLevel, starter CmdStarter,
logger openvpn.Logger) (runner *openvpn.Runner, connection models.Connection, err error,
) {
connection, err = providerConf.GetConnection(settings.Provider.ServerSelection, ipv6Supported)
ipv6Internet := ipv6SupportLevel == netlink.IPv6Internet
connection, err = providerConf.GetConnection(settings.Provider.ServerSelection, ipv6Internet)
if err != nil {
return nil, models.Connection{}, fmt.Errorf("finding a valid server connection: %w", err)
}
lines := providerConf.OpenVPNConfig(connection, settings.OpenVPN, ipv6Supported)
lines := providerConf.OpenVPNConfig(connection, settings.OpenVPN, ipv6SupportLevel.IsSupported())
if err := openvpnConf.WriteConfig(lines); err != nil {
return nil, models.Connection{}, fmt.Errorf("writing configuration to file: %w", err)

View File

@@ -36,11 +36,11 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
if settings.Type == vpn.OpenVPN {
vpnInterface = settings.OpenVPN.Interface
vpnRunner, connection, err = setupOpenVPN(ctx, l.fw,
l.openvpnConf, providerConf, settings, l.ipv6Supported, l.starter, subLogger)
l.openvpnConf, providerConf, settings, l.ipv6SupportLevel, l.starter, subLogger)
} else { // Wireguard
vpnInterface = settings.Wireguard.Interface
vpnRunner, connection, err = setupWireguard(ctx, l.netLinker, l.fw,
providerConf, settings, l.ipv6Supported, subLogger)
providerConf, settings, l.ipv6SupportLevel, subLogger)
}
if err != nil {
l.crashed(ctx, err)
@@ -56,14 +56,14 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
password: settings.Provider.PortForwarding.Password,
}
openvpnCtx, openvpnCancel := context.WithCancel(context.Background())
vpnCtx, vpnCancel := context.WithCancel(context.Background())
waitError := make(chan error)
tunnelReady := make(chan struct{})
go vpnRunner.Run(openvpnCtx, waitError, tunnelReady)
go vpnRunner.Run(vpnCtx, waitError, tunnelReady)
if err := l.waitForError(ctx, waitError); err != nil {
openvpnCancel()
vpnCancel()
l.crashed(ctx, err)
continue
}
@@ -75,10 +75,10 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
for stayHere {
select {
case <-tunnelReady:
go l.onTunnelUp(openvpnCtx, ctx, tunnelUpData)
go l.onTunnelUp(vpnCtx, ctx, tunnelUpData)
case <-ctx.Done():
l.cleanup()
openvpnCancel()
vpnCancel()
<-waitError
close(waitError)
return
@@ -86,7 +86,7 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
l.userTrigger = true
l.logger.Info("stopping")
l.cleanup()
openvpnCancel()
vpnCancel()
<-waitError
// do not close waitError or the waitError
// select case will trigger
@@ -99,7 +99,7 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
l.statusManager.Lock() // prevent SetStatus from running in parallel
l.cleanup()
openvpnCancel()
vpnCancel()
l.statusManager.SetStatus(constants.Crashed)
l.logAndWait(ctx, err)
stayHere = false
@@ -107,6 +107,6 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
l.statusManager.Unlock()
}
}
openvpnCancel()
vpnCancel()
}
}

View File

@@ -46,7 +46,7 @@ func (l *Loop) onTunnelUp(ctx, loopCtx context.Context, data tunnelUpData) {
return
}
if *l.dnsLooper.GetSettings().DoT.Enabled {
if *l.dnsLooper.GetSettings().ServerEnabled {
_, _ = l.dnsLooper.ApplyStatus(ctx, constants.Running)
} else {
err := check.WaitForDNS(ctx, check.Settings{})

View File

@@ -6,6 +6,7 @@ import (
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/netlink"
"github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/provider/utils"
"github.com/qdm12/gluetun/internal/wireguard"
@@ -16,15 +17,16 @@ import (
// It returns a serverName for port forwarding (PIA) and an error if it fails.
func setupWireguard(ctx context.Context, netlinker NetLinker,
fw Firewall, providerConf provider.Provider,
settings settings.VPN, ipv6Supported bool, logger wireguard.Logger) (
settings settings.VPN, ipv6SupportLevel netlink.IPv6SupportLevel, logger wireguard.Logger) (
wireguarder *wireguard.Wireguard, connection models.Connection, err error,
) {
connection, err = providerConf.GetConnection(settings.Provider.ServerSelection, ipv6Supported)
ipv6Internet := ipv6SupportLevel == netlink.IPv6Internet
connection, err = providerConf.GetConnection(settings.Provider.ServerSelection, ipv6Internet)
if err != nil {
return nil, models.Connection{}, fmt.Errorf("finding a VPN server: %w", err)
}
wireguardSettings := utils.BuildWireguardSettings(connection, settings.Wireguard, ipv6Supported)
wireguardSettings := utils.BuildWireguardSettings(connection, settings.Wireguard, ipv6SupportLevel.IsSupported())
logger.Debug("Wireguard server public key: " + wireguardSettings.PublicKey)
logger.Debug("Wireguard client private key: " + gosettings.ObfuscateKey(wireguardSettings.PrivateKey))

View File

@@ -1,6 +1,5 @@
# Maintenance
- Rename `UNBLOCK` to `DNS_HOSTNAMES_UNBLOCKED`
- Change `Run` methods to `Start`+`Stop`, returning channels rather than injecting them
- Go 1.18
- gofumpt