mirror of
https://github.com/qdm12/gluetun.git
synced 2026-08-01 18:03:20 +02:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d1e20c555 | |||
| 44143aa3ba | |||
| 56e5cad66b | |||
| c0073c9567 | |||
| cccd7c4c1b | |||
| 4ddf7c9a57 | |||
| a4df514e4b | |||
| 35847bb3b2 | |||
| d36d0f84d7 | |||
| e68b70ff8b | |||
| 73c3ce23da | |||
| 6857210429 | |||
| 0f7f35d6dc | |||
| ec71f1881b | |||
| 44f2df6f87 | |||
| 650c61e93c | |||
| 2f98539848 | |||
| 5c02e2106c | |||
| e6815054d9 | |||
| 9fe34266f0 | |||
| cdfd644c04 | |||
| 1c722b0f80 | |||
| 2e204e11b6 | |||
| c745f61156 | |||
| 4301196c21 | |||
| ef48826e48 | |||
| 3fca8b3dfe | |||
| 8ab97bb4b4 | |||
| 8269fad3b2 | |||
| 16870d6385 | |||
| d41081f4b3 | |||
| 5fbe277f9c | |||
| c5b20a3b06 | |||
| 1b871496ea | |||
| 05795ad2d1 | |||
| f9a86f48aa | |||
| 3a39236fa8 | |||
| 998ae46e97 | |||
| c4e65f8cc7 | |||
| a20795df9e | |||
| 78929cd6e1 | |||
| 75c2404179 | |||
| bcbc8c41f2 | |||
| 3da0c21c82 | |||
| 009b8e45b7 | |||
| 05dc21ed61 | |||
| 270dce9acb | |||
| 23cf3231f0 | |||
| 7f22fb3276 | |||
| 6909a0c123 | |||
| 3e1f48932a | |||
| 50744852c5 | |||
| 09e52bc685 | |||
| 857fe425ec |
@@ -59,19 +59,16 @@ jobs:
|
||||
- name: Run tests in test container
|
||||
run: |
|
||||
touch coverage.txt
|
||||
docker run --rm --cap-add=NET_ADMIN --device /dev/net/tun \
|
||||
docker run --rm --device /dev/net/tun \
|
||||
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
|
||||
test-container
|
||||
|
||||
- name: Verify dev cross platform compatibility
|
||||
run: docker build --target xcompile .
|
||||
|
||||
- name: Build final image
|
||||
run: docker build -t final-image .
|
||||
|
||||
verify-private:
|
||||
if: |
|
||||
github.repository == 'qdm12/gluetun' &&
|
||||
github.repository == 'passteque/gluetun' &&
|
||||
(
|
||||
github.event_name == 'push' ||
|
||||
github.event_name == 'release' ||
|
||||
@@ -118,7 +115,7 @@ jobs:
|
||||
|
||||
publish:
|
||||
if: |
|
||||
github.repository == 'qdm12/gluetun' &&
|
||||
github.repository == 'passteque/gluetun' &&
|
||||
(
|
||||
github.event_name == 'push' ||
|
||||
github.event_name == 'release' ||
|
||||
@@ -130,6 +127,7 @@ jobs:
|
||||
contents: read
|
||||
packages: write
|
||||
runs-on: ubuntu-latest
|
||||
environment: secrets
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
@@ -164,11 +162,15 @@ jobs:
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: qdm12
|
||||
password: ${{ github.token }}
|
||||
password: ${{ secrets.GHCR_PAT }}
|
||||
|
||||
- name: Short commit
|
||||
id: shortcommit
|
||||
run: echo "::set-output name=value::$(git rev-parse --short HEAD)"
|
||||
run: |
|
||||
# Use the PR head SHA if it exists, otherwise fallback to GITHUB_SHA
|
||||
FULL_SHA="${{ github.event.pull_request.head.sha || github.sha }}"
|
||||
SHORT_SHA=
|
||||
echo "value=$(echo $FULL_SHA | cut -c1-7)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push final image
|
||||
uses: docker/build-push-action@v6
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
config-file: .github/workflows/configs/mlc-config.json
|
||||
|
||||
- uses: peter-evans/dockerhub-description@v4
|
||||
if: github.repository == 'qdm12/gluetun' && github.event_name == 'push'
|
||||
if: github.repository == 'passteque/gluetun' && github.event_name == 'push'
|
||||
with:
|
||||
username: qmcgaw
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
@@ -46,10 +46,6 @@ RUN git init && \
|
||||
git diff --exit-code && \
|
||||
rm -rf .git/
|
||||
|
||||
FROM --platform=${BUILDPLATFORM} base AS xcompile
|
||||
RUN GOOS=darwin go build -o /dev/null ./...
|
||||
RUN GOOS=windows go build -o /dev/null ./...
|
||||
|
||||
FROM --platform=${BUILDPLATFORM} base AS build
|
||||
ARG TARGETPLATFORM
|
||||
ARG VERSION=unknown
|
||||
|
||||
+26
-38
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
@@ -16,6 +15,8 @@ import (
|
||||
_ "time/tzdata"
|
||||
|
||||
_ "github.com/breml/rootcerts"
|
||||
"github.com/qdm12/dns/v2/pkg/doh"
|
||||
dnsprovider "github.com/qdm12/dns/v2/pkg/provider"
|
||||
"github.com/qdm12/gluetun/internal/alpine"
|
||||
"github.com/qdm12/gluetun/internal/cli"
|
||||
"github.com/qdm12/gluetun/internal/command"
|
||||
@@ -40,7 +41,6 @@ import (
|
||||
"github.com/qdm12/gluetun/internal/server"
|
||||
"github.com/qdm12/gluetun/internal/shadowsocks"
|
||||
"github.com/qdm12/gluetun/internal/storage"
|
||||
"github.com/qdm12/gluetun/internal/tun"
|
||||
updater "github.com/qdm12/gluetun/internal/updater/loop"
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
"github.com/qdm12/gluetun/internal/updater/unzip"
|
||||
@@ -77,7 +77,6 @@ func main() {
|
||||
logger := log.New(log.SetLevel(log.LevelInfo))
|
||||
|
||||
args := os.Args
|
||||
tun := tun.New()
|
||||
netLinkDebugLogger := logger.New(log.SetComponent("netlink"))
|
||||
netLinker := netlink.New(netLinkDebugLogger)
|
||||
cli := cli.New()
|
||||
@@ -97,7 +96,7 @@ func main() {
|
||||
|
||||
errorCh := make(chan error)
|
||||
go func() {
|
||||
errorCh <- _main(ctx, buildInfo, args, logger, reader, tun, netLinker, cmder, cli)
|
||||
errorCh <- _main(ctx, buildInfo, args, logger, reader, netLinker, cmder, cli)
|
||||
}()
|
||||
|
||||
// Wait for OS signal or run error
|
||||
@@ -144,7 +143,7 @@ var errCommandUnknown = errors.New("command is unknown")
|
||||
//nolint:gocognit,gocyclo,maintidx
|
||||
func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
args []string, logger log.LoggerInterface, reader *reader.Reader,
|
||||
tun Tun, netLinker netLinker, cmder RunStarter,
|
||||
netLinker netLinker, cmder RunStarter,
|
||||
cli clier,
|
||||
) error {
|
||||
if len(args) > 1 { // cli operation
|
||||
@@ -264,7 +263,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
|
||||
puid, pgid := int(*allSettings.System.PUID), int(*allSettings.System.PGID)
|
||||
|
||||
const clientTimeout = 15 * time.Second
|
||||
const clientTimeout = 35 * time.Second
|
||||
httpClient := &http.Client{Timeout: clientTimeout}
|
||||
// Create configurators
|
||||
alpineConf := alpine.New()
|
||||
@@ -336,19 +335,6 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
return fmt.Errorf("adding local rules: %w", err)
|
||||
}
|
||||
|
||||
const tunDevice = "/dev/net/tun"
|
||||
err = tun.Check(tunDevice)
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("checking TUN device: %w (see the Wiki errors/tun page)", err)
|
||||
}
|
||||
logger.Info(err.Error() + "; creating it...")
|
||||
err = tun.Create(tunDevice)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating tun device: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, port := range allSettings.Firewall.InputPorts {
|
||||
for _, defaultRoute := range defaultRoutes {
|
||||
err = firewallConf.SetAllowedPort(ctx, port, defaultRoute.NetInterface)
|
||||
@@ -394,7 +380,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
}
|
||||
|
||||
dnsLogger := logger.New(log.SetComponent("dns"))
|
||||
dnsLooper, err := dns.NewLoop(allSettings.DNS, httpClient, firewallConf,
|
||||
dnsLooper, err := dns.NewLoop(allSettings.DNS, httpClient,
|
||||
dnsLogger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating DNS loop: %w", err)
|
||||
@@ -428,10 +414,18 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
go healthcheckServer.Run(healthServerCtx, healthServerDone)
|
||||
healthChecker := healthcheck.NewChecker(healthLogger)
|
||||
|
||||
// Note: we use a separate DoH dialer for the VPN servers data updater, separate from the
|
||||
// main DNS local server to make sure no request is blocked by filters.
|
||||
dohDialer, err := doh.New(doh.Settings{
|
||||
UpstreamResolvers: []dnsprovider.Provider{dnsprovider.Cloudflare(), dnsprovider.Google()},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating updater DoH dialer: %w", err)
|
||||
}
|
||||
updaterLogger := logger.New(log.SetComponent("updater"))
|
||||
|
||||
unzipper := unzip.New(httpClient)
|
||||
parallelResolver := resolver.NewParallelResolver(allSettings.Updater.DNSAddress)
|
||||
parallelResolver := resolver.NewParallelResolver(dohDialer)
|
||||
openvpnFileExtractor := extract.New()
|
||||
providers := provider.NewProviders(storage, time.Now, updaterLogger,
|
||||
httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(),
|
||||
@@ -554,26 +548,26 @@ type netLinker interface {
|
||||
Router
|
||||
Ruler
|
||||
Linker
|
||||
IsWireguardSupported() (ok bool, err error)
|
||||
IsWireguardSupported() bool
|
||||
IsIPv6Supported() (ok bool, err error)
|
||||
PatchLoggerLevel(level log.Level)
|
||||
}
|
||||
|
||||
type Addresser interface {
|
||||
AddrList(linkIndex uint32, family uint8) (
|
||||
addresses []netip.Prefix, err error)
|
||||
AddrReplace(linkIndex uint32, addr netip.Prefix) error
|
||||
AddrList(link netlink.Link, family int) (
|
||||
addresses []netlink.Addr, err error)
|
||||
AddrReplace(link netlink.Link, addr netlink.Addr) error
|
||||
}
|
||||
|
||||
type Router interface {
|
||||
RouteList(family uint8) (routes []netlink.Route, err error)
|
||||
RouteList(family int) (routes []netlink.Route, err error)
|
||||
RouteAdd(route netlink.Route) error
|
||||
RouteDel(route netlink.Route) error
|
||||
RouteReplace(route netlink.Route) error
|
||||
}
|
||||
|
||||
type Ruler interface {
|
||||
RuleList(family uint8) (rules []netlink.Rule, err error)
|
||||
RuleList(family int) (rules []netlink.Rule, err error)
|
||||
RuleAdd(rule netlink.Rule) error
|
||||
RuleDel(rule netlink.Rule) error
|
||||
}
|
||||
@@ -581,12 +575,11 @@ type Ruler interface {
|
||||
type Linker interface {
|
||||
LinkList() (links []netlink.Link, err error)
|
||||
LinkByName(name string) (link netlink.Link, err error)
|
||||
LinkByIndex(index uint32) (link netlink.Link, err error)
|
||||
LinkAdd(link netlink.Link) (linkIndex uint32, err error)
|
||||
LinkDel(linkIndex uint32) (err error)
|
||||
LinkSetUp(linkIndex uint32) (err error)
|
||||
LinkSetDown(linkIndex uint32) (err error)
|
||||
LinkSetMTU(linkIndex, mtu uint32) error
|
||||
LinkByIndex(index int) (link netlink.Link, err error)
|
||||
LinkAdd(link netlink.Link) (linkIndex int, err error)
|
||||
LinkDel(link netlink.Link) (err error)
|
||||
LinkSetUp(link netlink.Link) (linkIndex int, err error)
|
||||
LinkSetDown(link netlink.Link) (err error)
|
||||
}
|
||||
|
||||
type clier interface {
|
||||
@@ -598,11 +591,6 @@ type clier interface {
|
||||
GenKey(args []string) error
|
||||
}
|
||||
|
||||
type Tun interface {
|
||||
Check(tunDevice string) error
|
||||
Create(tunDevice string) error
|
||||
}
|
||||
|
||||
type RunStarter interface {
|
||||
Run(cmd *exec.Cmd) (output string, err error)
|
||||
Start(cmd *exec.Cmd) (stdoutLines, stderrLines <-chan string,
|
||||
|
||||
@@ -7,12 +7,10 @@ require (
|
||||
github.com/breml/rootcerts v0.3.3
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/jsimonetti/rtnetlink v1.4.2
|
||||
github.com/klauspost/compress v1.18.1
|
||||
github.com/klauspost/pgzip v1.2.6
|
||||
github.com/mdlayher/genetlink v1.3.2
|
||||
github.com/pelletier/go-toml/v2 v2.2.4
|
||||
github.com/qdm12/dns/v2 v2.0.0-rc10
|
||||
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a
|
||||
github.com/qdm12/gosettings v0.4.4
|
||||
github.com/qdm12/goshutdown v0.3.0
|
||||
github.com/qdm12/gosplash v0.2.0
|
||||
@@ -21,11 +19,12 @@ require (
|
||||
github.com/qdm12/ss-server v0.6.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
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
|
||||
golang.org/x/net v0.49.0
|
||||
golang.org/x/sys v0.40.0
|
||||
golang.org/x/text v0.33.0
|
||||
golang.org/x/net v0.47.0
|
||||
golang.org/x/sys v0.38.0
|
||||
golang.org/x/text v0.31.0
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
@@ -39,12 +38,13 @@ require (
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/cronokirby/saferith v0.33.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/josharian/native v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mdlayher/genetlink v1.3.2 // indirect
|
||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||
github.com/mdlayher/socket v0.5.1 // indirect
|
||||
github.com/mdlayher/socket v0.4.1 // indirect
|
||||
github.com/miekg/dns v1.1.62 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
@@ -55,11 +55,12 @@ require (
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 // indirect
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
google.golang.org/protobuf v1.35.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
@@ -13,8 +13,6 @@ github.com/breml/rootcerts v0.3.3/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXB
|
||||
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
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/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4=
|
||||
github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM=
|
||||
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
@@ -28,12 +26,10 @@ github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
||||
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
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/jsimonetti/rtnetlink v1.4.2 h1:Df9w9TZ3npHTyDn0Ev9e1uzmN2odmXd0QX+J5GTEn90=
|
||||
github.com/jsimonetti/rtnetlink v1.4.2/go.mod h1:92s6LJdE+1iOrw+F2/RO7LYI2Qd8pPpFNNUYW06gcoM=
|
||||
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=
|
||||
@@ -51,8 +47,8 @@ github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy5
|
||||
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
||||
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
||||
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
||||
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
||||
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
|
||||
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
|
||||
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
||||
@@ -73,8 +69,8 @@ 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-rc10 h1:IyeNEYXfhBsaE1dwxx5eAqdAz1HS98dT+8c7xoKODa0=
|
||||
github.com/qdm12/dns/v2 v2.0.0-rc10/go.mod h1:98foWgXJZ+g8gJIuO+fdO+oWpFei5WShMFTeN4Im2lE=
|
||||
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a h1:TE157yPQmAbVruH0MWCQzs0vTT/6t96DkoWUXd6PVuc=
|
||||
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a/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=
|
||||
@@ -97,6 +93,10 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
||||
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=
|
||||
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
||||
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
|
||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
@@ -106,15 +106,15 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
@@ -122,14 +122,14 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -140,10 +140,12 @@ golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
@@ -153,8 +155,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -162,8 +164,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
+20
-3
@@ -10,6 +10,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/doh"
|
||||
dnsprovider "github.com/qdm12/dns/v2/pkg/provider"
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||
@@ -38,12 +40,12 @@ type UpdaterLogger interface {
|
||||
func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error {
|
||||
options := settings.Updater{}
|
||||
var endUserMode, maintainerMode, updateAll bool
|
||||
var csvProviders, ipToken, protonUsername, protonEmail, protonPassword string
|
||||
var dnsServer, csvProviders, ipToken, protonUsername, protonEmail, protonPassword string
|
||||
flagSet := flag.NewFlagSet("update", flag.ExitOnError)
|
||||
flagSet.BoolVar(&endUserMode, "enduser", false, "Write results to /gluetun/servers.json (for end users)")
|
||||
flagSet.BoolVar(&maintainerMode, "maintainer", false,
|
||||
"Write results to ./internal/storage/servers.json to modify the program (for maintainers)")
|
||||
flagSet.StringVar(&options.DNSAddress, "dns", "8.8.8.8", "DNS resolver address to use")
|
||||
flagSet.StringVar(&dnsServer, "dns", "", "no longer used, your DNS will use DoH with Cloudflare and Google")
|
||||
const defaultMinRatio = 0.8
|
||||
flagSet.Float64Var(&options.MinRatio, "minratio", defaultMinRatio,
|
||||
"Minimum ratio of servers to find for the update to succeed")
|
||||
@@ -58,6 +60,10 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
||||
return err
|
||||
}
|
||||
|
||||
if dnsServer != "" {
|
||||
logger.Warn("The -dns flag is no longer used, your DNS will use DoH with Cloudflare and Google")
|
||||
}
|
||||
|
||||
if !endUserMode && !maintainerMode {
|
||||
return fmt.Errorf("%w", ErrModeUnspecified)
|
||||
}
|
||||
@@ -97,10 +103,21 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
|
||||
return fmt.Errorf("creating servers storage: %w", err)
|
||||
}
|
||||
|
||||
dohSettings := doh.Settings{
|
||||
UpstreamResolvers: []dnsprovider.Provider{
|
||||
dnsprovider.Cloudflare(),
|
||||
dnsprovider.Google(),
|
||||
},
|
||||
}
|
||||
dnsDialer, err := doh.New(dohSettings)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating DoH dialer: %w", err)
|
||||
}
|
||||
|
||||
const clientTimeout = 10 * time.Second
|
||||
httpClient := &http.Client{Timeout: clientTimeout}
|
||||
unzipper := unzip.New(httpClient)
|
||||
parallelResolver := resolver.NewParallelResolver(options.DNSAddress)
|
||||
parallelResolver := resolver.NewParallelResolver(dnsDialer)
|
||||
nameTokenPairs := []api.NameToken{
|
||||
{Name: string(api.IPInfo), Token: ipToken},
|
||||
{Name: string(api.IP2Location)},
|
||||
|
||||
+18
-21
@@ -9,8 +9,9 @@ import (
|
||||
)
|
||||
|
||||
// Start launches a command and streams stdout and stderr to channels.
|
||||
// All the channels returned are ready only and won't be closed
|
||||
// if the command fails later.
|
||||
// stdoutLines and stderrLines channels will be closed when there is no more
|
||||
// output to read, in order for the caller to catch all lines even after the
|
||||
// command has finished. The waitError channel returned will never be closed.
|
||||
func (c *Cmder) Start(cmd *exec.Cmd) (
|
||||
stdoutLines, stderrLines <-chan string,
|
||||
waitError <-chan error, startErr error,
|
||||
@@ -21,7 +22,6 @@ func (c *Cmder) Start(cmd *exec.Cmd) (
|
||||
func start(cmd execCmd) (stdoutLines, stderrLines <-chan string,
|
||||
waitError <-chan error, startErr error,
|
||||
) {
|
||||
stop := make(chan struct{})
|
||||
stdoutReady := make(chan struct{})
|
||||
stdoutLinesCh := make(chan string)
|
||||
stdoutDone := make(chan struct{})
|
||||
@@ -33,43 +33,47 @@ func start(cmd execCmd) (stdoutLines, stderrLines <-chan string,
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
go streamToChannel(stdoutReady, stop, stdoutDone, stdout, stdoutLinesCh)
|
||||
go streamToChannel(stdoutReady, stdoutDone, stdout, stdoutLinesCh)
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
_ = stdout.Close()
|
||||
close(stop)
|
||||
<-stdoutDone
|
||||
close(stdoutLinesCh)
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
go streamToChannel(stderrReady, stop, stderrDone, stderr, stderrLinesCh)
|
||||
go streamToChannel(stderrReady, stderrDone, stderr, stderrLinesCh)
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
_ = stdout.Close()
|
||||
_ = stderr.Close()
|
||||
close(stop)
|
||||
<-stdoutDone
|
||||
close(stdoutLinesCh)
|
||||
_ = stderr.Close()
|
||||
<-stderrDone
|
||||
close(stderrLinesCh)
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
waitErrorCh := make(chan error)
|
||||
go func() {
|
||||
err := cmd.Wait()
|
||||
_ = stdout.Close()
|
||||
_ = stderr.Close()
|
||||
close(stop)
|
||||
<-stdoutDone
|
||||
close(stdoutLinesCh)
|
||||
_ = stdout.Close()
|
||||
<-stderrDone
|
||||
close(stderrLinesCh)
|
||||
_ = stderr.Close()
|
||||
waitErrorCh <- err
|
||||
}()
|
||||
|
||||
<-stdoutReady
|
||||
<-stderrReady
|
||||
|
||||
return stdoutLinesCh, stderrLinesCh, waitErrorCh, nil
|
||||
}
|
||||
|
||||
func streamToChannel(ready chan<- struct{},
|
||||
stop <-chan struct{}, done chan<- struct{},
|
||||
func streamToChannel(ready chan<- struct{}, done chan<- struct{},
|
||||
stream io.Reader, lines chan<- string,
|
||||
) {
|
||||
defer close(done)
|
||||
@@ -89,12 +93,5 @@ func streamToChannel(ready chan<- struct{},
|
||||
if err == nil || errors.Is(err, os.ErrClosed) {
|
||||
return
|
||||
}
|
||||
|
||||
// ignore the error if it is stopped.
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
default:
|
||||
lines <- "stream error: " + err.Error()
|
||||
}
|
||||
lines <- "stream error: " + err.Error()
|
||||
}
|
||||
|
||||
@@ -89,30 +89,48 @@ func Test_start(t *testing.T) {
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
var stdoutIndex, stderrIndex int
|
||||
|
||||
done := false
|
||||
for !done {
|
||||
select {
|
||||
case line := <-stdoutLines:
|
||||
assert.Equal(t, testCase.stdout[stdoutIndex], line)
|
||||
stdoutIndex++
|
||||
case line := <-stderrLines:
|
||||
assert.Equal(t, testCase.stderr[stderrIndex], line)
|
||||
stderrIndex++
|
||||
case err := <-waitError:
|
||||
if testCase.waitErr != nil {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, testCase.waitErr.Error(), err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
done = true
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, len(testCase.stdout), stdoutIndex)
|
||||
assert.Equal(t, len(testCase.stderr), stderrIndex)
|
||||
collectAndCheckChannels(t, stdoutLines, stderrLines, waitError,
|
||||
testCase.stdout, testCase.stderr, testCase.waitErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func collectAndCheckChannels(t *testing.T, stdoutLines, stderrLines <-chan string,
|
||||
waitError <-chan error, expectedStdout, expectedStderr []string, expectedWaitErr error,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
stdoutIndex := 0
|
||||
stderrIndex := 0
|
||||
|
||||
done := false
|
||||
for !done {
|
||||
select {
|
||||
case line, ok := <-stdoutLines:
|
||||
if !ok {
|
||||
stdoutLines = nil
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, expectedStdout[stdoutIndex], line)
|
||||
stdoutIndex++
|
||||
case line, ok := <-stderrLines:
|
||||
if !ok {
|
||||
stderrLines = nil
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, expectedStderr[stderrIndex], line)
|
||||
stderrIndex++
|
||||
case err := <-waitError:
|
||||
if expectedWaitErr != nil {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, expectedWaitErr.Error(), err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
done = true
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, len(expectedStdout), stdoutIndex)
|
||||
assert.Equal(t, len(expectedStderr), stderrIndex)
|
||||
}
|
||||
|
||||
@@ -60,7 +60,6 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
||||
providers.Giganews,
|
||||
providers.Ipvanish,
|
||||
providers.Perfectprivacy,
|
||||
providers.Privado,
|
||||
providers.Vyprvpn,
|
||||
) {
|
||||
return fmt.Errorf("%w: for VPN service provider %s",
|
||||
@@ -75,8 +74,8 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
||||
providers.Privatevpn, providers.Torguard:
|
||||
// no custom port allowed
|
||||
case providers.Expressvpn, providers.Fastestvpn,
|
||||
providers.Giganews, providers.Ipvanish, providers.Nordvpn,
|
||||
providers.Privado, providers.Purevpn,
|
||||
providers.Giganews, providers.Ipvanish,
|
||||
providers.Nordvpn, providers.Purevpn,
|
||||
providers.Surfshark, providers.VPNSecure,
|
||||
providers.VPNUnlimited, providers.Vyprvpn:
|
||||
return fmt.Errorf("%w: for VPN service provider %s",
|
||||
@@ -99,8 +98,11 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
||||
case providers.Perfectprivacy:
|
||||
allowedTCP = []uint16{44, 443, 4433}
|
||||
allowedUDP = []uint16{44, 443, 4433}
|
||||
case providers.Privado:
|
||||
allowedTCP = []uint16{443, 1194, 8080, 8443}
|
||||
allowedUDP = []uint16{443, 1194, 8080, 8443}
|
||||
case providers.PrivateInternetAccess:
|
||||
allowedTCP = []uint16{80, 110, 443}
|
||||
allowedTCP = []uint16{80, 110, 443, 501, 502, 8443}
|
||||
allowedUDP = []uint16{53, 1194, 1197, 1198, 8080, 9201}
|
||||
case providers.Protonvpn:
|
||||
allowedTCP = []uint16{443, 5995, 8443}
|
||||
@@ -130,7 +132,6 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
||||
// Validate EncPreset
|
||||
if vpnProvider == providers.PrivateInternetAccess {
|
||||
validEncryptionPresets := []string{
|
||||
presets.None,
|
||||
presets.Normal,
|
||||
presets.Strong,
|
||||
}
|
||||
|
||||
@@ -48,10 +48,6 @@ func (p *Provider) validate(vpnType string, filterChoicesGetter FilterChoicesGet
|
||||
return fmt.Errorf("%w for Wireguard: %w", ErrVPNProviderNameNotValid, err)
|
||||
}
|
||||
|
||||
if p.Name == providers.Mullvad && vpnType == vpn.OpenVPN {
|
||||
warner.Warn("https://mullvad.net/en/blog/removing-openvpn-15th-january-2026")
|
||||
}
|
||||
|
||||
err = p.ServerSelection.validate(p.Name, filterChoicesGetter, warner)
|
||||
if err != nil {
|
||||
return fmt.Errorf("server selection: %w", err)
|
||||
|
||||
@@ -21,10 +21,6 @@ type Updater struct {
|
||||
// updater. It cannot be nil in the internal state.
|
||||
// TODO change to value and add Enabled field.
|
||||
Period *time.Duration
|
||||
// DNSAddress is the DNS server address to use
|
||||
// to resolve VPN server hostnames to IP addresses.
|
||||
// It cannot be the empty string in the internal state.
|
||||
DNSAddress string
|
||||
// MinRatio is the minimum ratio of servers to
|
||||
// find per provider, compared to the total current
|
||||
// number of servers. It defaults to 0.8.
|
||||
@@ -76,7 +72,6 @@ func (u Updater) Validate() (err error) {
|
||||
func (u *Updater) copy() (copied Updater) {
|
||||
return Updater{
|
||||
Period: gosettings.CopyPointer(u.Period),
|
||||
DNSAddress: u.DNSAddress,
|
||||
MinRatio: u.MinRatio,
|
||||
Providers: gosettings.CopySlice(u.Providers),
|
||||
ProtonEmail: gosettings.CopyPointer(u.ProtonEmail),
|
||||
@@ -89,7 +84,6 @@ func (u *Updater) copy() (copied Updater) {
|
||||
// settings.
|
||||
func (u *Updater) overrideWith(other Updater) {
|
||||
u.Period = gosettings.OverrideWithPointer(u.Period, other.Period)
|
||||
u.DNSAddress = gosettings.OverrideWithComparable(u.DNSAddress, other.DNSAddress)
|
||||
u.MinRatio = gosettings.OverrideWithComparable(u.MinRatio, other.MinRatio)
|
||||
u.Providers = gosettings.OverrideWithSlice(u.Providers, other.Providers)
|
||||
u.ProtonEmail = gosettings.OverrideWithPointer(u.ProtonEmail, other.ProtonEmail)
|
||||
@@ -98,7 +92,6 @@ func (u *Updater) overrideWith(other Updater) {
|
||||
|
||||
func (u *Updater) SetDefaults(vpnProvider string) {
|
||||
u.Period = gosettings.DefaultPointer(u.Period, 0)
|
||||
u.DNSAddress = gosettings.DefaultComparable(u.DNSAddress, "1.1.1.1:53")
|
||||
|
||||
if u.MinRatio == 0 {
|
||||
const defaultMinRatio = 0.8
|
||||
@@ -125,7 +118,6 @@ func (u Updater) toLinesNode() (node *gotree.Node) {
|
||||
|
||||
node = gotree.New("Server data updater settings:")
|
||||
node.Appendf("Update period: %s", *u.Period)
|
||||
node.Appendf("DNS address: %s", u.DNSAddress)
|
||||
node.Appendf("Minimum ratio: %.1f", u.MinRatio)
|
||||
node.Appendf("Providers to update: %s", strings.Join(u.Providers, ", "))
|
||||
if slices.Contains(u.Providers, providers.Protonvpn) {
|
||||
@@ -142,11 +134,6 @@ func (u *Updater) read(r *reader.Reader) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
u.DNSAddress, err = readUpdaterDNSAddress()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.MinRatio, err = r.Float64("UPDATER_MIN_RATIO")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -166,12 +153,3 @@ func (u *Updater) read(r *reader.Reader) (err error) {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readUpdaterDNSAddress() (address string, err error) {
|
||||
// TODO this is currently using Cloudflare in
|
||||
// plaintext to not be blocked by DNS over TLS by default.
|
||||
// If a plaintext address is set in the DNS settings, this one will be used.
|
||||
// use custom future encrypted DNS written in Go without blocking
|
||||
// as it's too much trouble to start another parallel unbound instance for now.
|
||||
return "", nil
|
||||
}
|
||||
|
||||
@@ -45,8 +45,7 @@ type Wireguard struct {
|
||||
// It has been lowered to 1320 following quite a bit of
|
||||
// investigation in the issue:
|
||||
// https://github.com/qdm12/gluetun/issues/2533.
|
||||
// Note this should now be replaced with the PMTUD feature.
|
||||
MTU uint32 `json:"mtu"`
|
||||
MTU uint16 `json:"mtu"`
|
||||
// Implementation is the Wireguard implementation to use.
|
||||
// It can be "auto", "userspace" or "kernelspace".
|
||||
// It defaults to "auto" and cannot be the empty string
|
||||
@@ -252,10 +251,12 @@ func (w *Wireguard) read(r *reader.Reader) (err error) {
|
||||
// WARNING: do not initialize w.Addresses to an empty slice
|
||||
// or the defaults for nordvpn will not work.
|
||||
for _, addressString := range addressStrings {
|
||||
if !strings.ContainsRune(addressString, '/') {
|
||||
addressString = strings.TrimSpace(addressString)
|
||||
if addressString == "" {
|
||||
continue
|
||||
} else if !strings.ContainsRune(addressString, '/') {
|
||||
addressString += "/32"
|
||||
}
|
||||
addressString = strings.TrimSpace(addressString)
|
||||
address, err := netip.ParsePrefix(addressString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing address: %w", err)
|
||||
@@ -273,7 +274,7 @@ func (w *Wireguard) read(r *reader.Reader) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
mtuPtr, err := r.Uint32Ptr("WIREGUARD_MTU")
|
||||
mtuPtr, err := r.Uint16Ptr("WIREGUARD_MTU")
|
||||
if err != nil {
|
||||
return err
|
||||
} else if mtuPtr != nil {
|
||||
|
||||
@@ -3,10 +3,10 @@ package files
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
@@ -82,12 +82,12 @@ func parseWireguardPeerSection(peerSection *ini.Section) (
|
||||
publicKey = getINIKeyFromSection(peerSection, "PublicKey")
|
||||
endpoint := getINIKeyFromSection(peerSection, "Endpoint")
|
||||
if endpoint != nil {
|
||||
parts := strings.Split(*endpoint, ":")
|
||||
endpointIP = &parts[0]
|
||||
const partsWithPort = 2
|
||||
if len(parts) >= partsWithPort {
|
||||
endpointPort = new(string)
|
||||
*endpointPort = strings.Join(parts[1:], ":")
|
||||
host, port, err := net.SplitHostPort(*endpoint)
|
||||
if err == nil {
|
||||
endpointIP = &host
|
||||
endpointPort = &port
|
||||
} else {
|
||||
endpointIP = endpoint
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -179,6 +179,12 @@ Endpoint = 1.2.3.4:51820`,
|
||||
endpointIP: ptrTo("1.2.3.4"),
|
||||
endpointPort: ptrTo("51820"),
|
||||
},
|
||||
"ipv6_endpoint": {
|
||||
iniData: `[Peer]
|
||||
Endpoint = [2a02:bbbb:aaaa:8075::10]:51820`,
|
||||
endpointIP: ptrTo("2a02:bbbb:aaaa:8075::10"),
|
||||
endpointPort: ptrTo("51820"),
|
||||
},
|
||||
}
|
||||
|
||||
for testName, testCase := range testCases {
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
type Logger interface {
|
||||
Debug(s string)
|
||||
Info(s string)
|
||||
Warn(s string)
|
||||
Error(s string)
|
||||
}
|
||||
|
||||
type Firewall interface {
|
||||
RestrictOutputAddrPort(ctx context.Context, addrPort netip.AddrPort) (err error)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package dns
|
||||
|
||||
type Logger interface {
|
||||
Debug(s string)
|
||||
Info(s string)
|
||||
Warn(s string)
|
||||
Error(s string)
|
||||
}
|
||||
@@ -24,7 +24,6 @@ type Loop struct {
|
||||
localResolvers []netip.Addr
|
||||
resolvConf string
|
||||
client *http.Client
|
||||
firewall Firewall
|
||||
logger Logger
|
||||
userTrigger bool
|
||||
start <-chan struct{}
|
||||
@@ -40,7 +39,7 @@ type Loop struct {
|
||||
const defaultBackoffTime = 10 * time.Second
|
||||
|
||||
func NewLoop(settings settings.DNS,
|
||||
client *http.Client, firewall Firewall, logger Logger,
|
||||
client *http.Client, logger Logger,
|
||||
) (loop *Loop, err error) {
|
||||
start := make(chan struct{})
|
||||
running := make(chan models.LoopStatus)
|
||||
@@ -65,7 +64,6 @@ func NewLoop(settings settings.DNS,
|
||||
filter: filter,
|
||||
resolvConf: "/etc/resolv.conf",
|
||||
client: client,
|
||||
firewall: firewall,
|
||||
logger: logger,
|
||||
userTrigger: true,
|
||||
start: start,
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/nameserver"
|
||||
)
|
||||
|
||||
func (l *Loop) useUnencryptedDNS(ctx context.Context, fallback bool) {
|
||||
func (l *Loop) useUnencryptedDNS(fallback bool) {
|
||||
settings := l.GetSettings()
|
||||
|
||||
targetIP := settings.GetFirstPlaintextIPv4()
|
||||
@@ -21,9 +20,8 @@ func (l *Loop) useUnencryptedDNS(ctx context.Context, fallback bool) {
|
||||
|
||||
const dialTimeout = 3 * time.Second
|
||||
const defaultDNSPort = 53
|
||||
addrPort := netip.AddrPortFrom(targetIP, defaultDNSPort)
|
||||
settingsInternalDNS := nameserver.SettingsInternalDNS{
|
||||
AddrPort: addrPort,
|
||||
AddrPort: netip.AddrPortFrom(targetIP, defaultDNSPort),
|
||||
Timeout: dialTimeout,
|
||||
}
|
||||
nameserver.UseDNSInternally(settingsInternalDNS)
|
||||
@@ -36,9 +34,4 @@ func (l *Loop) useUnencryptedDNS(ctx context.Context, fallback bool) {
|
||||
if err != nil {
|
||||
l.logger.Error(err.Error())
|
||||
}
|
||||
|
||||
err = l.firewall.RestrictOutputAddrPort(ctx, addrPort)
|
||||
if err != nil {
|
||||
l.logger.Error("restricting plain DNS traffic to " + targetIP.String() + ": " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
+4
-10
@@ -2,7 +2,6 @@ package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/nameserver"
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
@@ -24,7 +23,7 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||
"and go through your container network DNS outside the VPN tunnel!")
|
||||
} else {
|
||||
const fallback = false
|
||||
l.useUnencryptedDNS(ctx, fallback)
|
||||
l.useUnencryptedDNS(fallback)
|
||||
}
|
||||
|
||||
select {
|
||||
@@ -53,11 +52,6 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !errors.Is(err, errUpdateBlockLists) {
|
||||
const fallback = true
|
||||
l.useUnencryptedDNS(ctx, fallback)
|
||||
}
|
||||
l.logAndWait(ctx, err)
|
||||
settings = l.GetSettings()
|
||||
}
|
||||
@@ -66,7 +60,7 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||
settings = l.GetSettings()
|
||||
if !*settings.KeepNameserver && !*settings.ServerEnabled {
|
||||
const fallback = false
|
||||
l.useUnencryptedDNS(ctx, fallback)
|
||||
l.useUnencryptedDNS(fallback)
|
||||
}
|
||||
|
||||
l.userTrigger = false
|
||||
@@ -94,7 +88,7 @@ func (l *Loop) runWait(ctx context.Context, runError <-chan error) (exitLoop boo
|
||||
settings := l.GetSettings()
|
||||
if !*settings.KeepNameserver && *settings.ServerEnabled {
|
||||
const fallback = false
|
||||
l.useUnencryptedDNS(ctx, fallback)
|
||||
l.useUnencryptedDNS(fallback)
|
||||
l.stopServer()
|
||||
}
|
||||
l.stopped <- struct{}{}
|
||||
@@ -105,7 +99,7 @@ func (l *Loop) runWait(ctx context.Context, runError <-chan error) (exitLoop boo
|
||||
case err := <-runError: // unexpected error
|
||||
l.statusManager.SetStatus(constants.Crashed)
|
||||
const fallback = true
|
||||
l.useUnencryptedDNS(ctx, fallback)
|
||||
l.useUnencryptedDNS(fallback)
|
||||
l.logAndWait(ctx, err)
|
||||
return false
|
||||
}
|
||||
|
||||
+11
-13
@@ -2,24 +2,28 @@ package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/check"
|
||||
"github.com/qdm12/dns/v2/pkg/middlewares/filter/update"
|
||||
"github.com/qdm12/dns/v2/pkg/nameserver"
|
||||
"github.com/qdm12/dns/v2/pkg/server"
|
||||
)
|
||||
|
||||
var errUpdateBlockLists = errors.New("cannot update filter block lists")
|
||||
|
||||
func (l *Loop) setupServer(ctx context.Context) (runError <-chan error, err error) {
|
||||
err = l.updateFiles(ctx)
|
||||
settings := l.GetSettings()
|
||||
var updateSettings update.Settings
|
||||
updateSettings.SetRebindingProtectionExempt(settings.Blacklist.RebindingProtectionExemptHostnames)
|
||||
err = l.filter.Update(updateSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", errUpdateBlockLists, err)
|
||||
return nil, fmt.Errorf("updating filter for rebinding protection: %w", err)
|
||||
}
|
||||
|
||||
settings := l.GetSettings()
|
||||
err = l.updateFiles(ctx, settings)
|
||||
if err != nil {
|
||||
l.logger.Warn("downloading block lists failed, skipping: " + err.Error())
|
||||
}
|
||||
|
||||
serverSettings, err := buildServerSettings(settings, l.filter, l.localResolvers, l.logger)
|
||||
if err != nil {
|
||||
@@ -39,9 +43,8 @@ func (l *Loop) setupServer(ctx context.Context) (runError <-chan error, err erro
|
||||
|
||||
// use internal DNS server
|
||||
const defaultDNSPort = 53
|
||||
addrPort := netip.AddrPortFrom(settings.ServerAddress, defaultDNSPort)
|
||||
nameserver.UseDNSInternally(nameserver.SettingsInternalDNS{
|
||||
AddrPort: addrPort,
|
||||
AddrPort: netip.AddrPortFrom(settings.ServerAddress, defaultDNSPort),
|
||||
})
|
||||
err = nameserver.UseDNSSystemWide(nameserver.SettingsSystemDNS{
|
||||
IPs: []netip.Addr{settings.ServerAddress},
|
||||
@@ -51,11 +54,6 @@ func (l *Loop) setupServer(ctx context.Context) (runError <-chan error, err erro
|
||||
l.logger.Error(err.Error())
|
||||
}
|
||||
|
||||
err = l.firewall.RestrictOutputAddrPort(ctx, addrPort)
|
||||
if err != nil {
|
||||
l.logger.Error("restricting plain DNS traffic to " + addrPort.Addr().String() + ": " + err.Error())
|
||||
}
|
||||
|
||||
err = check.WaitForDNS(ctx, check.Settings{})
|
||||
if err != nil {
|
||||
l.stopServer()
|
||||
|
||||
@@ -30,8 +30,9 @@ func (l *Loop) RunRestartTicker(ctx context.Context, done chan<- struct{}) {
|
||||
lastTick = l.timeNow()
|
||||
|
||||
status := l.GetStatus()
|
||||
settings := l.GetSettings()
|
||||
if status == constants.Running {
|
||||
if err := l.updateFiles(ctx); err != nil {
|
||||
if err := l.updateFiles(ctx, settings); err != nil {
|
||||
l.statusManager.SetStatus(constants.Crashed)
|
||||
l.logger.Error(err.Error())
|
||||
l.logger.Warn("skipping DNS server restart due to failed files update")
|
||||
@@ -44,7 +45,6 @@ func (l *Loop) RunRestartTicker(ctx context.Context, done chan<- struct{}) {
|
||||
_, _ = l.statusManager.ApplyStatus(ctx, constants.Stopped)
|
||||
_, _ = l.statusManager.ApplyStatus(ctx, constants.Running)
|
||||
|
||||
settings := l.GetSettings()
|
||||
timer.Reset(*settings.UpdatePeriod)
|
||||
case <-l.updateTicker:
|
||||
if !timer.Stop() {
|
||||
|
||||
@@ -6,11 +6,10 @@ import (
|
||||
|
||||
"github.com/qdm12/dns/v2/pkg/blockbuilder"
|
||||
"github.com/qdm12/dns/v2/pkg/middlewares/filter/update"
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
)
|
||||
|
||||
func (l *Loop) updateFiles(ctx context.Context) (err error) {
|
||||
settings := l.GetSettings()
|
||||
|
||||
func (l *Loop) updateFiles(ctx context.Context, settings settings.DNS) (err error) {
|
||||
l.logger.Info("downloading hostnames and IP block lists")
|
||||
blacklistSettings := settings.Blacklist.ToBlockBuilderSettings(l.client)
|
||||
|
||||
@@ -37,7 +36,6 @@ func (l *Loop) updateFiles(ctx context.Context) (err error) {
|
||||
IPPrefixes: result.BlockedIPPrefixes,
|
||||
}
|
||||
updateSettings.BlockHostnames(result.BlockedHostnames)
|
||||
updateSettings.SetRebindingProtectionExempt(settings.Blacklist.RebindingProtectionExemptHostnames)
|
||||
err = l.filter.Update(updateSettings)
|
||||
if err != nil {
|
||||
return fmt.Errorf("updating filter: %w", err)
|
||||
|
||||
@@ -69,8 +69,8 @@ func Test_deleteIPTablesRule(t *testing.T) {
|
||||
"invalid_instruction": {
|
||||
instruction: "invalid",
|
||||
errWrapped: ErrIptablesCommandMalformed,
|
||||
errMessage: "parsing iptables command: parsing \"invalid\": " +
|
||||
"iptables command is malformed: flag \"invalid\" requires a value, but got none",
|
||||
errMessage: "parsing iptables command: iptables command is malformed: " +
|
||||
"fields count 1 is not even: \"invalid\"",
|
||||
},
|
||||
"list_error": {
|
||||
instruction: "-t nat --delete PREROUTING -i tun0 -p tcp --dport 43716 -j REDIRECT --to-ports 5678",
|
||||
|
||||
@@ -10,12 +10,11 @@ import (
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
runner CmdRunner
|
||||
logger Logger
|
||||
iptablesMutex sync.Mutex
|
||||
ip6tablesMutex sync.Mutex
|
||||
defaultRoutes []routing.DefaultRoute
|
||||
localNetworks []routing.LocalNetwork
|
||||
runner CmdRunner
|
||||
logger Logger
|
||||
iptablesMutex sync.Mutex
|
||||
defaultRoutes []routing.DefaultRoute
|
||||
localNetworks []routing.LocalNetwork
|
||||
|
||||
// Fixed state
|
||||
ipTables string
|
||||
@@ -29,7 +28,6 @@ type Config struct {
|
||||
outboundSubnets []netip.Prefix
|
||||
allowedInputPorts map[uint16]map[string]struct{} // port to interfaces set mapping
|
||||
portRedirections portRedirections
|
||||
outputAddrPort map[uint16]netip.Addr
|
||||
stateMutex sync.Mutex
|
||||
}
|
||||
|
||||
@@ -53,7 +51,6 @@ func NewConfig(ctx context.Context, logger Logger,
|
||||
runner: runner,
|
||||
logger: logger,
|
||||
allowedInputPorts: make(map[uint16]map[string]struct{}),
|
||||
outputAddrPort: make(map[uint16]netip.Addr),
|
||||
ipTables: iptables,
|
||||
ip6Tables: ip6tables,
|
||||
customRulesPath: "/iptables/post-rules.txt",
|
||||
|
||||
@@ -36,8 +36,8 @@ func (c *Config) runIP6tablesInstruction(ctx context.Context, instruction string
|
||||
if c.ip6Tables == "" {
|
||||
return nil
|
||||
}
|
||||
c.ip6tablesMutex.Lock() // only one ip6tables command at once
|
||||
defer c.ip6tablesMutex.Unlock()
|
||||
c.iptablesMutex.Lock() // only one ip6tables command at once
|
||||
defer c.iptablesMutex.Unlock()
|
||||
|
||||
if isDeleteMatchInstruction(instruction) {
|
||||
return deleteIPTablesRule(ctx, c.ip6Tables, instruction,
|
||||
|
||||
@@ -148,9 +148,6 @@ func (c *Config) acceptOutputTrafficToVPN(ctx context.Context,
|
||||
defaultInterface string, connection models.Connection, remove bool,
|
||||
) error {
|
||||
protocol := connection.Protocol
|
||||
if protocol == "tcp-client" {
|
||||
protocol = "tcp"
|
||||
}
|
||||
instruction := fmt.Sprintf("%s OUTPUT -d %s -o %s -p %s -m %s --dport %d -j ACCEPT",
|
||||
appendOrDelete(remove), connection.IP, defaultInterface, protocol,
|
||||
protocol, connection.Port)
|
||||
@@ -218,6 +215,9 @@ func (c *Config) redirectPort(ctx context.Context, intf string,
|
||||
interfaceFlag = ""
|
||||
}
|
||||
|
||||
c.iptablesMutex.Lock()
|
||||
defer c.iptablesMutex.Unlock()
|
||||
|
||||
err = c.runIptablesInstructions(ctx, []string{
|
||||
fmt.Sprintf("-t nat %s PREROUTING %s -p tcp --dport %d -j REDIRECT --to-ports %d",
|
||||
appendOrDelete(remove), interfaceFlag, sourcePort, destinationPort),
|
||||
|
||||
@@ -2,7 +2,6 @@ package firewall
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func (c *Config) runMixedIptablesInstructions(ctx context.Context, instructions []string) error {
|
||||
@@ -20,15 +19,3 @@ func (c *Config) runMixedIptablesInstruction(ctx context.Context, instruction st
|
||||
}
|
||||
return c.runIP6tablesInstruction(ctx, instruction)
|
||||
}
|
||||
|
||||
func (c *Config) runIPv4AndV6IptablesInstructions(ctx context.Context,
|
||||
ipv4Instructions, ipv6Instructions []string,
|
||||
) error {
|
||||
if err := c.runIptablesInstructions(ctx, ipv4Instructions); err != nil {
|
||||
return fmt.Errorf("running iptables instructions: %w", err)
|
||||
}
|
||||
if err := c.runIP6tablesInstructions(ctx, ipv6Instructions); err != nil {
|
||||
return fmt.Errorf("running ip6tables instructions: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+19
-111
@@ -9,19 +9,9 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type operation uint8
|
||||
|
||||
const (
|
||||
opNone operation = iota
|
||||
opAppend
|
||||
opDelete
|
||||
opInsert
|
||||
opReplace
|
||||
)
|
||||
|
||||
type iptablesInstruction struct {
|
||||
table string // defaults to "filter", and can be "nat" for example.
|
||||
operation operation
|
||||
append bool
|
||||
chain string // for example INPUT, PREROUTING. Cannot be empty.
|
||||
target string // for example ACCEPT. Can be empty.
|
||||
protocol string // "tcp" or "udp" or "" for all protocols.
|
||||
@@ -32,7 +22,6 @@ type iptablesInstruction struct {
|
||||
destinationPort uint16 // if zero, there is no destination port
|
||||
toPorts []uint16 // if empty, there is no redirection
|
||||
ctstate []string // if empty, there is no ctstate
|
||||
lineNumber uint16 // for replace operation, the line number to replace
|
||||
}
|
||||
|
||||
func (i *iptablesInstruction) setDefaults() {
|
||||
@@ -71,58 +60,6 @@ func (i *iptablesInstruction) equalToRule(table, chain string, rule chainRule) (
|
||||
}
|
||||
}
|
||||
|
||||
func (i *iptablesInstruction) String() string {
|
||||
var sb strings.Builder
|
||||
if i.table != "" && i.table != "filter" {
|
||||
sb.WriteString(fmt.Sprintf("-t %s ", i.table))
|
||||
}
|
||||
switch i.operation {
|
||||
case opNone:
|
||||
panic("no operation specified")
|
||||
case opAppend:
|
||||
sb.WriteString(fmt.Sprintf("--append %s ", i.chain))
|
||||
case opDelete:
|
||||
sb.WriteString(fmt.Sprintf("--delete %s ", i.chain))
|
||||
case opInsert:
|
||||
sb.WriteString(fmt.Sprintf("--insert %s ", i.chain))
|
||||
case opReplace:
|
||||
sb.WriteString(fmt.Sprintf("--replace %s %d ", i.chain, i.lineNumber))
|
||||
}
|
||||
if i.inputInterface != "" {
|
||||
sb.WriteString(fmt.Sprintf("-i %s ", i.inputInterface))
|
||||
}
|
||||
if i.outputInterface != "" {
|
||||
sb.WriteString(fmt.Sprintf("-o %s ", i.outputInterface))
|
||||
}
|
||||
if i.protocol != "" {
|
||||
sb.WriteString(fmt.Sprintf("-p %s ", i.protocol))
|
||||
}
|
||||
if i.source.IsValid() {
|
||||
sb.WriteString(fmt.Sprintf("-s %s ", i.source.String()))
|
||||
}
|
||||
if i.destination.IsValid() {
|
||||
sb.WriteString(fmt.Sprintf("-d %s ", i.destination.String()))
|
||||
}
|
||||
if i.destinationPort != 0 {
|
||||
sb.WriteString(fmt.Sprintf("--dport %d ", i.destinationPort))
|
||||
}
|
||||
if len(i.ctstate) > 0 {
|
||||
sb.WriteString(fmt.Sprintf("--ctstate %s ", strings.Join(i.ctstate, ",")))
|
||||
}
|
||||
if len(i.toPorts) > 0 {
|
||||
var portStrings []string
|
||||
for _, port := range i.toPorts {
|
||||
portStrings = append(portStrings, strconv.FormatUint(uint64(port), 10))
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("--to-ports %s ", strings.Join(portStrings, ",")))
|
||||
}
|
||||
if i.target != "" {
|
||||
sb.WriteString(fmt.Sprintf("-j %s ", i.target))
|
||||
}
|
||||
|
||||
return strings.TrimSpace(sb.String())
|
||||
}
|
||||
|
||||
// instruction can be "" which equivalent to the "*" chain rule interface.
|
||||
func networkInterfacesEqual(instruction, chainRule string) bool {
|
||||
return instruction == chainRule || (instruction == "" && chainRule == "*")
|
||||
@@ -140,63 +77,34 @@ func parseIptablesInstruction(s string) (instruction iptablesInstruction, err er
|
||||
return iptablesInstruction{}, fmt.Errorf("%w: empty instruction", ErrIptablesCommandMalformed)
|
||||
}
|
||||
fields := strings.Fields(s)
|
||||
if len(fields)%2 != 0 {
|
||||
return iptablesInstruction{}, fmt.Errorf("%w: fields count %d is not even: %q",
|
||||
ErrIptablesCommandMalformed, len(fields), s)
|
||||
}
|
||||
|
||||
i := 0
|
||||
for i < len(fields) {
|
||||
consumed, err := parseInstructionFlag(fields[i:], &instruction)
|
||||
for i := 0; i < len(fields); i += 2 {
|
||||
key := fields[i]
|
||||
value := fields[i+1]
|
||||
err = parseInstructionFlag(key, value, &instruction)
|
||||
if err != nil {
|
||||
return iptablesInstruction{}, fmt.Errorf("parsing %q: %w", s, err)
|
||||
}
|
||||
i += consumed
|
||||
}
|
||||
|
||||
instruction.setDefaults()
|
||||
return instruction, nil
|
||||
}
|
||||
|
||||
func parseInstructionFlag(fields []string, instruction *iptablesInstruction) (consumed int, err error) {
|
||||
flag := fields[0]
|
||||
|
||||
// All flags use one value after the flag, except the following:
|
||||
switch flag {
|
||||
case "-R", "--replace":
|
||||
const expected = 3
|
||||
if len(fields) < expected {
|
||||
return 0, fmt.Errorf("%w: flag %q requires at least 2 values, but got %s",
|
||||
ErrIptablesCommandMalformed, flag, strings.Join(fields, " "))
|
||||
}
|
||||
consumed = expected
|
||||
default:
|
||||
const expected = 2
|
||||
if len(fields) < expected {
|
||||
return 0, fmt.Errorf("%w: flag %q requires a value, but got none",
|
||||
ErrIptablesCommandMalformed, flag)
|
||||
}
|
||||
consumed = expected
|
||||
}
|
||||
value := fields[1]
|
||||
|
||||
switch flag {
|
||||
func parseInstructionFlag(key, value string, instruction *iptablesInstruction) (err error) {
|
||||
switch key {
|
||||
case "-t", "--table":
|
||||
instruction.table = value
|
||||
case "-D", "--delete":
|
||||
instruction.operation = opDelete
|
||||
instruction.append = false
|
||||
instruction.chain = value
|
||||
case "-A", "--append":
|
||||
instruction.operation = opAppend
|
||||
instruction.append = true
|
||||
instruction.chain = value
|
||||
case "-I", "--insert":
|
||||
instruction.operation = opInsert
|
||||
instruction.chain = value
|
||||
case "-R", "--replace":
|
||||
instruction.operation = opReplace
|
||||
instruction.chain = value
|
||||
const base, bits = 10, 16
|
||||
n, err := strconv.ParseUint(fields[2], base, bits)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("parsing line number for --replace operation: %w", err)
|
||||
}
|
||||
instruction.lineNumber = uint16(n)
|
||||
case "-j", "--jump":
|
||||
instruction.target = value
|
||||
case "-p", "--protocol":
|
||||
@@ -209,18 +117,18 @@ func parseInstructionFlag(fields []string, instruction *iptablesInstruction) (co
|
||||
case "-s", "--source":
|
||||
instruction.source, err = parseIPPrefix(value)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("parsing source IP CIDR: %w", err)
|
||||
return fmt.Errorf("parsing source IP CIDR: %w", err)
|
||||
}
|
||||
case "-d", "--destination":
|
||||
instruction.destination, err = parseIPPrefix(value)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("parsing destination IP CIDR: %w", err)
|
||||
return fmt.Errorf("parsing destination IP CIDR: %w", err)
|
||||
}
|
||||
case "--dport":
|
||||
const base, bitLength = 10, 16
|
||||
destinationPort, err := strconv.ParseUint(value, base, bitLength)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("parsing destination port: %w", err)
|
||||
return fmt.Errorf("parsing destination port: %w", err)
|
||||
}
|
||||
instruction.destinationPort = uint16(destinationPort)
|
||||
case "--ctstate":
|
||||
@@ -232,14 +140,14 @@ func parseInstructionFlag(fields []string, instruction *iptablesInstruction) (co
|
||||
const base, bitLength = 10, 16
|
||||
port, err := strconv.ParseUint(portString, base, bitLength)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("parsing port redirection: %w", err)
|
||||
return fmt.Errorf("parsing port redirection: %w", err)
|
||||
}
|
||||
instruction.toPorts[i] = uint16(port)
|
||||
}
|
||||
default:
|
||||
return 0, fmt.Errorf("%w: unknown flag %q", ErrIptablesCommandMalformed, flag)
|
||||
return fmt.Errorf("%w: unknown key %q", ErrIptablesCommandMalformed, key)
|
||||
}
|
||||
return consumed, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseIPPrefix(value string) (prefix netip.Prefix, err error) {
|
||||
|
||||
@@ -23,19 +23,19 @@ func Test_parseIptablesInstruction(t *testing.T) {
|
||||
"uneven_fields": {
|
||||
s: "-A",
|
||||
errWrapped: ErrIptablesCommandMalformed,
|
||||
errMessage: "parsing \"-A\": iptables command is malformed: flag \"-A\" requires a value, but got none",
|
||||
errMessage: "iptables command is malformed: fields count 1 is not even: \"-A\"",
|
||||
},
|
||||
"unknown_key": {
|
||||
s: "-x something",
|
||||
errWrapped: ErrIptablesCommandMalformed,
|
||||
errMessage: "parsing \"-x something\": iptables command is malformed: unknown flag \"-x\"",
|
||||
errMessage: "parsing \"-x something\": iptables command is malformed: unknown key \"-x\"",
|
||||
},
|
||||
"one_pair": {
|
||||
s: "-I INPUT",
|
||||
s: "-A INPUT",
|
||||
instruction: iptablesInstruction{
|
||||
table: "filter",
|
||||
chain: "INPUT",
|
||||
operation: opInsert,
|
||||
table: "filter",
|
||||
chain: "INPUT",
|
||||
append: true,
|
||||
},
|
||||
},
|
||||
"instruction_A": {
|
||||
@@ -43,7 +43,7 @@ func Test_parseIptablesInstruction(t *testing.T) {
|
||||
instruction: iptablesInstruction{
|
||||
table: "filter",
|
||||
chain: "INPUT",
|
||||
operation: opAppend,
|
||||
append: true,
|
||||
inputInterface: "tun0",
|
||||
protocol: "tcp",
|
||||
source: netip.MustParsePrefix("1.2.3.4/32"),
|
||||
@@ -57,7 +57,7 @@ func Test_parseIptablesInstruction(t *testing.T) {
|
||||
instruction: iptablesInstruction{
|
||||
table: "nat",
|
||||
chain: "PREROUTING",
|
||||
operation: opDelete,
|
||||
append: false,
|
||||
inputInterface: "tun0",
|
||||
protocol: "tcp",
|
||||
destinationPort: 43716,
|
||||
|
||||
@@ -3,7 +3,6 @@ package firewall
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
@@ -82,133 +81,3 @@ func (c *Config) RemoveAllowedPort(ctx context.Context, port uint16) (err error)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RestrictOutputAddrPort allows outgoing traffic to a specific IP and port for both tcp and udp,
|
||||
// while blocking other tcp or udp traffic to that port going to other IP addresses, both IPv4 and IPv6.
|
||||
// If the port was previously allowed for another IP address, that previous allowance will be removed.
|
||||
// Giving an invalid address will remove any existing restrictions for the port specified.
|
||||
func (c *Config) RestrictOutputAddrPort(ctx context.Context, addrPort netip.AddrPort) (err error) {
|
||||
c.stateMutex.Lock()
|
||||
defer c.stateMutex.Unlock()
|
||||
existingIP := c.outputAddrPort[addrPort.Port()]
|
||||
|
||||
switch {
|
||||
case existingIP == addrPort.Addr():
|
||||
return nil
|
||||
case !addrPort.Addr().IsValid():
|
||||
// invalid address, remove any existing rules for the port
|
||||
return c.removeOutputAddrPortRestriction(ctx, existingIP, addrPort.Port())
|
||||
case !existingIP.IsValid():
|
||||
// no previous existing address for the port
|
||||
return c.insertOutputAddrPortRestriction(ctx, addrPort)
|
||||
default:
|
||||
// existing rule in the same IP family or different family
|
||||
return c.replaceOutputAddrPortRestriction(ctx, existingIP, addrPort)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) removeOutputAddrPortRestriction(ctx context.Context, existingIP netip.Addr, port uint16) (err error) {
|
||||
commonInstructions := []string{
|
||||
fmt.Sprintf("--delete OUTPUT -p udp --dport %d -j DROP", port),
|
||||
fmt.Sprintf("--delete OUTPUT -p tcp --dport %d -j DROP", port),
|
||||
}
|
||||
ipv4Instructions := commonInstructions
|
||||
ipv6Instructions := commonInstructions
|
||||
|
||||
familySpecificInstructions := []string{
|
||||
fmt.Sprintf("--delete OUTPUT -p udp --dport %d -d %s -j ACCEPT", port, existingIP),
|
||||
fmt.Sprintf("--delete OUTPUT -p tcp --dport %d -d %s -j ACCEPT", port, existingIP),
|
||||
}
|
||||
if existingIP.Is4() {
|
||||
ipv4Instructions = append(ipv4Instructions, familySpecificInstructions...)
|
||||
} else {
|
||||
ipv6Instructions = append(ipv6Instructions, familySpecificInstructions...)
|
||||
}
|
||||
|
||||
err = c.runIPv4AndV6IptablesInstructions(ctx, ipv4Instructions, ipv6Instructions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
delete(c.outputAddrPort, port)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) insertOutputAddrPortRestriction(ctx context.Context, addrPort netip.AddrPort) (err error) {
|
||||
commonInstructions := []string{
|
||||
fmt.Sprintf("--insert OUTPUT -p udp --dport %d -j DROP", addrPort.Port()),
|
||||
fmt.Sprintf("--insert OUTPUT -p tcp --dport %d -j DROP", addrPort.Port()),
|
||||
}
|
||||
ipv4Instructions := commonInstructions
|
||||
ipv6Instructions := commonInstructions
|
||||
|
||||
familySpecificInstructions := []string{
|
||||
fmt.Sprintf("--insert OUTPUT -p udp --dport %d -d %s -j ACCEPT", addrPort.Port(), addrPort.Addr()),
|
||||
fmt.Sprintf("--insert OUTPUT -p tcp --dport %d -d %s -j ACCEPT", addrPort.Port(), addrPort.Addr()),
|
||||
}
|
||||
if addrPort.Addr().Is4() {
|
||||
ipv4Instructions = append(ipv4Instructions, familySpecificInstructions...)
|
||||
} else {
|
||||
ipv6Instructions = append(ipv6Instructions, familySpecificInstructions...)
|
||||
}
|
||||
err = c.runIPv4AndV6IptablesInstructions(ctx, ipv4Instructions, ipv6Instructions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.outputAddrPort[addrPort.Port()] = addrPort.Addr()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) replaceOutputAddrPortRestriction(ctx context.Context,
|
||||
existingIP netip.Addr, addrPort netip.AddrPort,
|
||||
) (err error) {
|
||||
for _, protocol := range [...]string{"udp", "tcp"} {
|
||||
switch {
|
||||
case existingIP.Is4() && addrPort.Addr().Is4():
|
||||
oldInstruction := fmt.Sprintf("--insert OUTPUT -p %s --dport %d -d %s -j ACCEPT",
|
||||
protocol, addrPort.Port(), existingIP)
|
||||
newInstruction := fmt.Sprintf("--insert OUTPUT -p %s --dport %d -d %s -j ACCEPT",
|
||||
protocol, addrPort.Port(), addrPort.Addr())
|
||||
err = c.replaceIptablesRule(ctx, oldInstruction, newInstruction)
|
||||
if err != nil {
|
||||
return fmt.Errorf("replacing existing IPv4 rule: %w", err)
|
||||
}
|
||||
case existingIP.Is6() && addrPort.Addr().Is6():
|
||||
oldInstruction := fmt.Sprintf("--insert OUTPUT -p %s --dport %d -d %s -j ACCEPT",
|
||||
protocol, addrPort.Port(), existingIP)
|
||||
newInstruction := fmt.Sprintf("--insert OUTPUT -p %s --dport %d -d %s -j ACCEPT",
|
||||
protocol, addrPort.Port(), addrPort.Addr())
|
||||
err = c.replaceIP6tablesRule(ctx, oldInstruction, newInstruction)
|
||||
if err != nil {
|
||||
return fmt.Errorf("replacing existing IPv6 rule: %w", err)
|
||||
}
|
||||
case existingIP.Is4() && addrPort.Addr().Is6():
|
||||
instruction := fmt.Sprintf("--delete OUTPUT -p %s --dport %d -d %s -j ACCEPT",
|
||||
protocol, addrPort.Port(), existingIP)
|
||||
err = c.runIptablesInstruction(ctx, instruction)
|
||||
if err != nil {
|
||||
return fmt.Errorf("removing existing IPv4 rule: %w", err)
|
||||
}
|
||||
instruction = fmt.Sprintf("--insert OUTPUT -p %s --dport %d -d %s -j ACCEPT",
|
||||
protocol, addrPort.Port(), addrPort.Addr())
|
||||
err = c.runIP6tablesInstruction(ctx, instruction)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inserting new IPv6 rule: %w", err)
|
||||
}
|
||||
case existingIP.Is6() && addrPort.Addr().Is4():
|
||||
instruction := fmt.Sprintf("--delete OUTPUT -p %s --dport %d -d %s -j ACCEPT",
|
||||
protocol, addrPort.Port(), existingIP)
|
||||
err = c.runIP6tablesInstruction(ctx, instruction)
|
||||
if err != nil {
|
||||
return fmt.Errorf("removing existing IPv6 rule: %w", err)
|
||||
}
|
||||
instruction = fmt.Sprintf("--insert OUTPUT -p %s --dport %d -d %s -j ACCEPT",
|
||||
protocol, addrPort.Port(), addrPort.Addr())
|
||||
err = c.runIptablesInstruction(ctx, instruction)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inserting new IPv4 rule: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
c.outputAddrPort[addrPort.Port()] = addrPort.Addr()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
package firewall
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var errRuleNotFound = errors.New("rule not found")
|
||||
|
||||
func (c *Config) replaceIptablesRule(ctx context.Context, oldInstruction, newInstruction string) error {
|
||||
targetRule, err := parseIptablesInstruction(oldInstruction)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing iptables command to replace: %w", err)
|
||||
}
|
||||
|
||||
lineNumber, err := findLineNumber(ctx, c.ipTables, targetRule, c.runner, c.logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding to-be-replaced chain rule line number: %w", err)
|
||||
} else if lineNumber == 0 {
|
||||
return fmt.Errorf("%w: matching to-be-replaced instruction %q", errRuleNotFound, oldInstruction)
|
||||
}
|
||||
parsed, err := parseIptablesInstruction(newInstruction)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing replacement iptables command: %w", err)
|
||||
}
|
||||
parsed.operation = opReplace
|
||||
parsed.lineNumber = lineNumber
|
||||
return c.runIptablesInstruction(ctx, parsed.String())
|
||||
}
|
||||
|
||||
func (c *Config) replaceIP6tablesRule(ctx context.Context, oldInstruction, newInstruction string) error {
|
||||
targetRule, err := parseIptablesInstruction(oldInstruction)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing iptables command to replace: %w", err)
|
||||
}
|
||||
|
||||
lineNumber, err := findLineNumber(ctx, c.ip6Tables, targetRule, c.runner, c.logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding to-be-replaced chain rule line number: %w", err)
|
||||
} else if lineNumber == 0 {
|
||||
return fmt.Errorf("%w: matching to-be-replaced instruction %q", errRuleNotFound, oldInstruction)
|
||||
}
|
||||
parsed, err := parseIptablesInstruction(newInstruction)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing replacement iptables command: %w", err)
|
||||
}
|
||||
parsed.operation = opReplace
|
||||
parsed.lineNumber = lineNumber
|
||||
return c.runIP6tablesInstruction(ctx, parsed.String())
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/healthcheck/dns"
|
||||
@@ -23,7 +22,7 @@ type Checker struct {
|
||||
logger Logger
|
||||
icmpTargetIPs []netip.Addr
|
||||
smallCheckType string
|
||||
configMutex sync.Mutex
|
||||
startupOnFail bool
|
||||
|
||||
icmpNotPermitted *bool
|
||||
|
||||
@@ -45,26 +44,41 @@ func NewChecker(logger Logger) *Checker {
|
||||
}
|
||||
}
|
||||
|
||||
// SetConfig sets the TCP+TLS dial addresses, the ICMP echo IP address
|
||||
// to target and the desired small check type (dns or icmp).
|
||||
// SetConfig sets the following:
|
||||
// - TCP+TLS dial addresses
|
||||
// - ICMP echo IP addresses to target
|
||||
// - the desired small check type (dns or icmp)
|
||||
// - whether to startup the periodic checks if the startup check fails.
|
||||
// This function MUST be called before calling [Checker.Start].
|
||||
func (c *Checker) SetConfig(tlsDialAddrs []string, icmpTargets []netip.Addr,
|
||||
smallCheckType string,
|
||||
smallCheckType string, startupOnFail bool,
|
||||
) {
|
||||
c.configMutex.Lock()
|
||||
defer c.configMutex.Unlock()
|
||||
c.tlsDialAddrs = tlsDialAddrs
|
||||
c.icmpTargetIPs = icmpTargets
|
||||
c.smallCheckType = smallCheckType
|
||||
c.startupOnFail = startupOnFail
|
||||
}
|
||||
|
||||
// Start starts the checker by first running a blocking 6s-timed TCP+TLS check,
|
||||
// and, on success, starts the periodic checks in a separate goroutine:
|
||||
// Start starts the [Checker] which behaves differently according to its
|
||||
// internal field startupOnFail, which is set by calling [Checker.SetConfig].
|
||||
//
|
||||
// By default, startupOnFail should be false and the behavior is as follows:
|
||||
// A blocking 6s-timed TCP+TLS check is performed first. If it fails,
|
||||
// an error is returned and the [Checker] is not started.
|
||||
// On success, it starts the periodic checks in a separate goroutine, returning
|
||||
// the runError error channel and a nil error.
|
||||
//
|
||||
// If startupOnFail is true, the behavior is as follows:
|
||||
// A blocking 6s-timed TCP+TLS check is performed first. If it fails,
|
||||
// the error is sent to the runError channel, but no error is returned
|
||||
// and the [Checker] continues to start the periodic checks in a separate goroutine, returning
|
||||
// the runError error channel and a nil error.
|
||||
//
|
||||
// The periodic checks consist in:
|
||||
// - a "small" ICMP echo check every minute
|
||||
// - a "full" TCP+TLS check every 5 minutes
|
||||
// It returns a channel `runError` that receives an error (nil or not) when a periodic check is performed.
|
||||
// It returns an error if the initial TCP+TLS check fails.
|
||||
// The Checker has to be ultimately stopped by calling [Checker.Stop].
|
||||
//
|
||||
// The [Checker] has to be ultimately stopped by calling [Checker.Stop].
|
||||
func (c *Checker) Start(ctx context.Context) (runError <-chan error, err error) {
|
||||
if len(c.tlsDialAddrs) == 0 || len(c.icmpTargetIPs) == 0 || c.smallCheckType == "" {
|
||||
panic("call Checker.SetConfig with non empty values before Checker.Start")
|
||||
@@ -76,9 +90,19 @@ func (c *Checker) Start(ctx context.Context) (runError <-chan error, err error)
|
||||
}
|
||||
c.echoer.Reset()
|
||||
|
||||
// runErrorCh MUST be buffered in the case startupOnFail is true, and
|
||||
// a startup error was encountered, to avoid blocking the startup
|
||||
// goroutine when sending the error, especially since the caller may
|
||||
// not be ready to receive from the channel yet.
|
||||
runErrorCh := make(chan error, 1)
|
||||
runError = runErrorCh
|
||||
err = c.startupCheck(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("startup check: %w", err)
|
||||
err = fmt.Errorf("startup check: %w", err)
|
||||
if !c.startupOnFail {
|
||||
return nil, err
|
||||
}
|
||||
runErrorCh <- err
|
||||
}
|
||||
|
||||
ready := make(chan struct{})
|
||||
@@ -90,8 +114,6 @@ func (c *Checker) Start(ctx context.Context) (runError <-chan error, err error)
|
||||
smallCheckTimer := time.NewTimer(smallCheckPeriod)
|
||||
const fullCheckPeriod = 5 * time.Minute
|
||||
fullCheckTimer := time.NewTimer(fullCheckPeriod)
|
||||
runErrorCh := make(chan error)
|
||||
runError = runErrorCh
|
||||
go func() {
|
||||
defer close(done)
|
||||
close(ready)
|
||||
@@ -140,10 +162,8 @@ func (c *Checker) Stop() error {
|
||||
}
|
||||
|
||||
func (c *Checker) smallPeriodicCheck(ctx context.Context) error {
|
||||
c.configMutex.Lock()
|
||||
icmpTargetIPs := make([]netip.Addr, len(c.icmpTargetIPs))
|
||||
copy(icmpTargetIPs, c.icmpTargetIPs)
|
||||
c.configMutex.Unlock()
|
||||
tryTimeouts := []time.Duration{
|
||||
5 * time.Second,
|
||||
5 * time.Second,
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrHTTPStatusNotOK = errors.New("HTTP response status is not OK")
|
||||
@@ -21,6 +22,9 @@ func NewClient(httpClient *http.Client) *Client {
|
||||
}
|
||||
|
||||
func (c *Client) Check(ctx context.Context, url string) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Second)
|
||||
defer cancel()
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package mod
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var errBuiltinModuleNotFound = errors.New("builtin module not found")
|
||||
|
||||
func checkModulesBuiltin(modulesPath, moduleName string) error {
|
||||
f, err := os.Open(filepath.Join(modulesPath, "modules.builtin"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
moduleName = strings.TrimSuffix(moduleName, ".ko")
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
line = strings.TrimSuffix(line, ".ko")
|
||||
if strings.HasSuffix(line, "/"+moduleName) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("%w: %s", errBuiltinModuleNotFound, moduleName)
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package mod
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
errModuleNameUnknown = errors.New("unknown module name")
|
||||
errKernelFeatureIsModule = errors.New("kernel feature is a module, not built-in")
|
||||
errKernelFeatureNotSet = errors.New("kernel feature not set")
|
||||
errKernelFeatureNotFound = errors.New("kernel feature not found")
|
||||
)
|
||||
|
||||
// checkProcConfig checks /proc/config.gz for a the kernel feature corresponding
|
||||
// to the given module name. If the kernel feature is found and set to "y", it returns nil.
|
||||
// If the kernel feature is found and set to "m", it returns an error indicating that the kernel
|
||||
// feature is a module, not built-in.
|
||||
// If the kernel feature is found and not set, it returns an error indicating that the kernel
|
||||
// feature is not set. If the kernel feature is not found, it returns an error indicating that the kernel
|
||||
// feature is not found.
|
||||
func checkProcConfig(moduleName string) error {
|
||||
f, err := os.Open("/proc/config.gz")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
gz, err := gzip.NewReader(f)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating gzip reader: %w", err)
|
||||
}
|
||||
defer gz.Close()
|
||||
|
||||
// If any group of kernel features is satisfied, then the module is considered supported.
|
||||
kernelFeatureGroups, ok := moduleNameToKernelFeatureGroups(moduleName)
|
||||
if !ok {
|
||||
return fmt.Errorf("%w: %s", errModuleNameUnknown, moduleName)
|
||||
}
|
||||
groups := make([]map[string]bool, len(kernelFeatureGroups))
|
||||
for i, group := range kernelFeatureGroups {
|
||||
featureToOK := make(map[string]bool)
|
||||
for _, feature := range group {
|
||||
featureToOK[feature] = false
|
||||
}
|
||||
groups[i] = featureToOK
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(gz)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
for _, featureToOK := range groups {
|
||||
for name, ok := range featureToOK {
|
||||
switch {
|
||||
case ok:
|
||||
case strings.HasPrefix(line, name+"=m"):
|
||||
return fmt.Errorf("%w: %s", errKernelFeatureIsModule, name)
|
||||
case strings.HasPrefix(line, name+"=y"):
|
||||
featureToOK[name] = true
|
||||
if allFeaturesOK(featureToOK) {
|
||||
return nil
|
||||
}
|
||||
case strings.HasPrefix(line, "# "+name+" is not set"):
|
||||
return fmt.Errorf("%w: %s", errKernelFeatureNotSet, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("%w: for module name %s", errKernelFeatureNotFound, moduleName)
|
||||
}
|
||||
|
||||
func moduleNameToKernelFeatureGroups(moduleName string) (featureGroups [][]string, ok bool) {
|
||||
moduleMap := map[string][][]string{
|
||||
"nf_tables": {{"CONFIG_NF_TABLES"}},
|
||||
|
||||
// Netfilter Matches
|
||||
"xt_conntrack": {{"CONFIG_NETFILTER_XT_MATCH_CONNTRACK"}},
|
||||
"xt_connmark": {
|
||||
{"CONFIG_NETFILTER_XT_CONNMARK"},
|
||||
{"CONFIG_NETFILTER_XT_MATCH_CONNMARK", "CONFIG_NETFILTER_XT_TARGET_CONNMARK"},
|
||||
},
|
||||
"xt_mark": {
|
||||
{"CONFIG_NETFILTER_XT_MARK"},
|
||||
{"CONFIG_NETFILTER_XT_MATCH_MARK", "CONFIG_NETFILTER_XT_TARGET_MARK"},
|
||||
},
|
||||
"nf_conntrack_netlink": {{"CONFIG_NF_CT_NETLINK"}},
|
||||
"nf_reject_ipv4": {{"CONFIG_NF_REJECT_IPV4"}},
|
||||
|
||||
// Common Netfilter Targets
|
||||
"xt_log": {{"CONFIG_NETFILTER_XT_TARGET_LOG"}},
|
||||
"xt_reject": {
|
||||
{"CONFIG_IP_NF_TARGET_REJECT", "CONFIG_NF_REJECT_IPV4"},
|
||||
{"CONFIG_NETFILTER_XT_TARGET_REJECT", "CONFIG_NF_REJECT_IPV4"},
|
||||
},
|
||||
"xt_masquerade": {{"CONFIG_NETFILTER_XT_TARGET_MASQUERADE"}},
|
||||
|
||||
// Additional Netfilter Matches
|
||||
"xt_addrtype": {{"CONFIG_NETFILTER_XT_MATCH_ADDRTYPE"}},
|
||||
"xt_comment": {{"CONFIG_NETFILTER_XT_MATCH_COMMENT"}},
|
||||
"xt_multiport": {{"CONFIG_NETFILTER_XT_MATCH_MULTIPORT"}},
|
||||
"xt_state": {{"CONFIG_NETFILTER_XT_MATCH_STATE"}},
|
||||
"xt_tcpudp": {{"CONFIG_NETFILTER_XT_MATCH_TCPUDP"}},
|
||||
|
||||
// Tunneling and Virtualization
|
||||
"tun": {{"CONFIG_TUN"}},
|
||||
"bridge": {{"CONFIG_BRIDGE"}},
|
||||
"veth": {{"CONFIG_VETH"}},
|
||||
"vxlan": {{"CONFIG_VXLAN"}},
|
||||
"wireguard": {{"CONFIG_WIREGUARD"}},
|
||||
|
||||
// Filesystems
|
||||
"overlay": {{"CONFIG_OVERLAY_FS"}},
|
||||
"fuse": {{"CONFIG_FUSE_FS"}},
|
||||
}
|
||||
|
||||
featureGroups, ok = moduleMap[strings.ToLower(moduleName)]
|
||||
return featureGroups, ok
|
||||
}
|
||||
|
||||
func allFeaturesOK(featureToOK map[string]bool) bool {
|
||||
for _, ok := range featureToOK {
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
//go:build !windows
|
||||
|
||||
package mod
|
||||
|
||||
import (
|
||||
@@ -30,36 +28,7 @@ type moduleInfo struct {
|
||||
|
||||
var ErrModulesDirectoryNotFound = errors.New("modules directory not found")
|
||||
|
||||
func getModulesInfo() (modulesInfo map[string]moduleInfo, err error) {
|
||||
var utsName unix.Utsname
|
||||
err = unix.Uname(&utsName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting unix uname release: %w", err)
|
||||
}
|
||||
release := unix.ByteSliceToString(utsName.Release[:])
|
||||
release = strings.TrimSpace(release)
|
||||
|
||||
modulePaths := []string{
|
||||
filepath.Join("/lib/modules", release),
|
||||
filepath.Join("/usr/lib/modules", release),
|
||||
}
|
||||
|
||||
var modulesPath string
|
||||
var found bool
|
||||
for _, modulesPath = range modulePaths {
|
||||
info, err := os.Stat(modulesPath)
|
||||
if err == nil && info.IsDir() {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return nil, fmt.Errorf("%w: %s are not valid existing directories"+
|
||||
"; have you bind mounted the /lib/modules directory?",
|
||||
ErrModulesDirectoryNotFound, strings.Join(modulePaths, ", "))
|
||||
}
|
||||
|
||||
func getModulesInfo(modulesPath string) (modulesInfo map[string]moduleInfo, err error) {
|
||||
dependencyFilepath := filepath.Join(modulesPath, "modules.dep")
|
||||
dependencyFile, err := os.Open(dependencyFilepath)
|
||||
if err != nil {
|
||||
@@ -111,6 +80,39 @@ func getModulesInfo() (modulesInfo map[string]moduleInfo, err error) {
|
||||
return modulesInfo, nil
|
||||
}
|
||||
|
||||
func getModulesPath() (string, error) {
|
||||
release, err := getReleaseName()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("getting release name: %w", err)
|
||||
}
|
||||
|
||||
modulePaths := []string{
|
||||
filepath.Join("/lib/modules", release),
|
||||
filepath.Join("/usr/lib/modules", release),
|
||||
}
|
||||
|
||||
for _, modulesPath := range modulePaths {
|
||||
info, err := os.Stat(modulesPath)
|
||||
if err == nil && info.IsDir() {
|
||||
return modulesPath, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("%w: %s are not valid existing directories"+
|
||||
"; have you bind mounted the /lib/modules directory?",
|
||||
ErrModulesDirectoryNotFound, strings.Join(modulePaths, ", "))
|
||||
}
|
||||
|
||||
func getReleaseName() (release string, err error) {
|
||||
var utsName unix.Utsname
|
||||
err = unix.Uname(&utsName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("getting unix uname release: %w", err)
|
||||
}
|
||||
release = unix.ByteSliceToString(utsName.Release[:])
|
||||
release = strings.TrimSpace(release)
|
||||
return release, nil
|
||||
}
|
||||
|
||||
func getBuiltinModules(modulesDirPath string, modulesInfo map[string]moduleInfo) error {
|
||||
file, err := os.Open(filepath.Join(modulesDirPath, "modules.builtin"))
|
||||
if err != nil {
|
||||
@@ -0,0 +1,74 @@
|
||||
package mod
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Probe is a expanded version of modprobe, in which it checks if the Kernel
|
||||
// built-in features contain the given module name.
|
||||
// It first tries to locate the modules directory in [getModulesPath].
|
||||
// If it fails (like on WSL), it then only checks for the kernel feature
|
||||
// in /proc/config.gz with [checkProcConfig].
|
||||
// Otherwise, it first checks if the modules directory modules.builtin
|
||||
// file contains the given module name in [checkModulesBuiltin].
|
||||
// If the module is not found, it then runs the classic [modProbe] behavior,
|
||||
// trying to load the module in the kernel.
|
||||
// If this fails, it does one final try running [checkProcConfig].
|
||||
func Probe(moduleName string) error {
|
||||
modulesPath, err := getModulesPath()
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrModulesDirectoryNotFound) {
|
||||
err = checkProcConfig(moduleName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking /proc/config.gz: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("getting modules path: %w", err)
|
||||
}
|
||||
|
||||
err = checkModulesBuiltin(modulesPath, moduleName)
|
||||
if err != nil {
|
||||
err = modProbe(modulesPath, moduleName)
|
||||
if err != nil {
|
||||
err = checkProcConfig(moduleName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking /proc/config.gz: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// modProbe is the classic modprobe behavior.
|
||||
func modProbe(modulesPath, moduleName string) error {
|
||||
modulesInfo, err := getModulesInfo(modulesPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting modules information: %w", err)
|
||||
}
|
||||
|
||||
modulePath, err := findModulePath(moduleName, modulesInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding module path: %w", err)
|
||||
}
|
||||
|
||||
info := modulesInfo[modulePath]
|
||||
if info.state == builtin || info.state == loaded {
|
||||
return nil
|
||||
}
|
||||
|
||||
info.state = loading
|
||||
for _, dependencyModulePath := range info.dependencyPaths {
|
||||
err = initDependencies(dependencyModulePath, modulesInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("init dependencies: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = initModule(modulePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("init module: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package mod
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Probe loads the given kernel module and its dependencies.
|
||||
func Probe(moduleName string) error {
|
||||
modulesInfo, err := getModulesInfo()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting modules information: %w", err)
|
||||
}
|
||||
|
||||
modulePath, err := findModulePath(moduleName, modulesInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding module path: %w", err)
|
||||
}
|
||||
|
||||
info := modulesInfo[modulePath]
|
||||
if info.state == builtin || info.state == loaded {
|
||||
return nil
|
||||
}
|
||||
|
||||
info.state = loading
|
||||
for _, dependencyModulePath := range info.dependencyPaths {
|
||||
err = initDependencies(dependencyModulePath, modulesInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("init dependencies: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = initModule(modulePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("init module: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
//go:build !linux
|
||||
|
||||
package mod
|
||||
|
||||
func Probe(moduleName string) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
+17
-59
@@ -1,75 +1,33 @@
|
||||
//go:build linux || darwin
|
||||
|
||||
package netlink
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
"github.com/jsimonetti/rtnetlink/rtnl"
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
func (n *NetLink) AddrList(linkIndex uint32, family uint8) (
|
||||
ipPrefixes []netip.Prefix, err error,
|
||||
func (n *NetLink) AddrList(link Link, family int) (
|
||||
addresses []Addr, err error,
|
||||
) {
|
||||
conn, err := rtnl.Dial(nil)
|
||||
netlinkLink := linkToNetlinkLink(&link)
|
||||
netlinkAddresses, err := netlink.AddrList(netlinkLink, family)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dialing netlink: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
ifc := &net.Interface{
|
||||
Index: int(linkIndex),
|
||||
}
|
||||
ipNets, err := conn.Addrs(ifc, int(family))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list addresses: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ipPrefixes = make([]netip.Prefix, len(ipNets))
|
||||
for i := range ipNets {
|
||||
ipPrefixes[i] = netIPNetToNetipPrefix(ipNets[i])
|
||||
addresses = make([]Addr, len(netlinkAddresses))
|
||||
for i := range netlinkAddresses {
|
||||
addresses[i].Network = netIPNetToNetipPrefix(netlinkAddresses[i].IPNet)
|
||||
}
|
||||
|
||||
return ipPrefixes, nil
|
||||
return addresses, nil
|
||||
}
|
||||
|
||||
func (n *NetLink) AddrReplace(linkIndex uint32, prefix netip.Prefix) error {
|
||||
conn, err := rtnl.Dial(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dialing netlink: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
ipNet := netipPrefixToIPNet(prefix)
|
||||
|
||||
// Remove any address identical to the one we want to add
|
||||
family := FamilyV4
|
||||
if prefix.Addr().Is6() {
|
||||
family = FamilyV6
|
||||
}
|
||||
ifc := &net.Interface{
|
||||
Index: int(linkIndex),
|
||||
}
|
||||
addresses, err := conn.Addrs(ifc, int(family))
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing addresses: %w", err)
|
||||
}
|
||||
for _, address := range addresses {
|
||||
if address.IP.Equal(ipNet.IP) &&
|
||||
net.IP(address.Mask).String() == net.IP(ipNet.Mask).String() {
|
||||
err = conn.AddrDel(ifc, address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting address from interface: %w", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
func (n *NetLink) AddrReplace(link Link, addr Addr) error {
|
||||
netlinkLink := linkToNetlinkLink(&link)
|
||||
netlinkAddress := netlink.Addr{
|
||||
IPNet: netipPrefixToIPNet(addr.Network),
|
||||
}
|
||||
|
||||
// Add the new address to the interface
|
||||
err = conn.AddrAdd(ifc, ipNet)
|
||||
if err != nil {
|
||||
return fmt.Errorf("adding address to interface: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return netlink.AddrReplace(netlinkLink, &netlinkAddress)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
//go:build !linux && !darwin
|
||||
|
||||
package netlink
|
||||
|
||||
func (n *NetLink) AddrList(link Link, family int) (
|
||||
addresses []Addr, err error,
|
||||
) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (n *NetLink) AddrReplace(Link, Addr) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
@@ -36,30 +36,6 @@ func netIPNetToNetipPrefix(ipNet *net.IPNet) (prefix netip.Prefix) {
|
||||
return netip.PrefixFrom(ip, bits)
|
||||
}
|
||||
|
||||
func ipAndLengthToPrefix(ip *net.IP, length uint8) netip.Prefix {
|
||||
if ip == nil || len(*ip) == 0 {
|
||||
return netip.Prefix{}
|
||||
}
|
||||
var dstIP netip.Addr
|
||||
if ipv4 := ip.To4(); ipv4 != nil { // IPv6
|
||||
dstIP = netip.AddrFrom4([4]byte(*ip))
|
||||
} else {
|
||||
dstIP = netip.AddrFrom16([16]byte(*ip))
|
||||
}
|
||||
return netip.PrefixFrom(dstIP, int(length))
|
||||
}
|
||||
|
||||
func prefixToIPAndLength(prefix netip.Prefix) (ip *net.IP, length uint8) {
|
||||
if !prefix.IsValid() {
|
||||
return nil, 0
|
||||
}
|
||||
prefixIP := prefix.Addr().Unmap()
|
||||
ip = new(net.IP)
|
||||
*ip = netipAddrToNetIP(prefixIP)
|
||||
length = uint8(prefix.Bits()) //nolint:gosec
|
||||
return ip, length
|
||||
}
|
||||
|
||||
func netipAddrToNetIP(address netip.Addr) (ip net.IP) {
|
||||
switch {
|
||||
case !address.IsValid():
|
||||
|
||||
@@ -4,7 +4,13 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func FamilyToString(family uint8) string {
|
||||
const (
|
||||
FamilyAll = 0
|
||||
FamilyV4 = 2
|
||||
FamilyV6 = 10
|
||||
)
|
||||
|
||||
func FamilyToString(family int) string {
|
||||
switch family {
|
||||
case FamilyAll:
|
||||
return "all"
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package netlink
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
const (
|
||||
FamilyAll uint8 = unix.AF_UNSPEC
|
||||
FamilyV4 uint8 = unix.AF_INET
|
||||
FamilyV6 uint8 = unix.AF_INET6
|
||||
)
|
||||
@@ -1,30 +1,16 @@
|
||||
package netlink
|
||||
|
||||
import (
|
||||
"math/rand/v2"
|
||||
"net/netip"
|
||||
|
||||
"github.com/qdm12/log"
|
||||
)
|
||||
|
||||
func ptrTo[T any](v T) *T { return &v }
|
||||
|
||||
func makeNetipPrefix(n byte) netip.Prefix {
|
||||
const bits = 24
|
||||
return netip.PrefixFrom(netip.AddrFrom4([4]byte{n, n, n, 0}), bits)
|
||||
}
|
||||
|
||||
var rng = rand.New(rand.NewChaCha8([32]byte{})) //nolint:gosec,gochecknoglobals
|
||||
|
||||
func makeLinkName() string {
|
||||
const alphabet = "abcdefghijklmnopqrstuvwxyz"
|
||||
name := make([]byte, 8)
|
||||
for i := range name {
|
||||
name[i] = alphabet[rng.IntN(len(alphabet))]
|
||||
}
|
||||
return "test" + string(name)
|
||||
}
|
||||
|
||||
type noopLogger struct{}
|
||||
|
||||
func (l *noopLogger) Debug(_ string) {}
|
||||
|
||||
@@ -19,7 +19,7 @@ func (n *NetLink) IsIPv6Supported() (supported bool, err error) {
|
||||
return false, fmt.Errorf("finding link corresponding to route: %w", err)
|
||||
}
|
||||
|
||||
sourceIsIPv6 := route.Src.Addr().IsValid() && route.Src.Addr().Is6()
|
||||
sourceIsIPv6 := route.Src.IsValid() && route.Src.Is6()
|
||||
destinationIsIPv6 := route.Dst.IsValid() && route.Dst.Addr().Is6()
|
||||
switch {
|
||||
case !sourceIsIPv6 && !destinationIsIPv6,
|
||||
|
||||
+76
-162
@@ -1,191 +1,105 @@
|
||||
//go:build linux || darwin
|
||||
|
||||
package netlink
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/jsimonetti/rtnetlink"
|
||||
)
|
||||
|
||||
type DeviceType uint16
|
||||
|
||||
type Link struct {
|
||||
Index uint32
|
||||
Name string
|
||||
DeviceType DeviceType
|
||||
VirtualType string
|
||||
MTU uint32
|
||||
}
|
||||
import "github.com/vishvananda/netlink"
|
||||
|
||||
func (n *NetLink) LinkList() (links []Link, err error) {
|
||||
conn, err := rtnetlink.Dial(nil)
|
||||
netlinkLinks, err := netlink.LinkList()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dialing netlink: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
linkMessages, err := conn.Link.List()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listing interfaces: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
links = make([]Link, len(linkMessages))
|
||||
for i, message := range linkMessages {
|
||||
virtualType := ""
|
||||
if message.Attributes.Info != nil {
|
||||
virtualType = message.Attributes.Info.Kind
|
||||
}
|
||||
links[i] = Link{
|
||||
Index: message.Index,
|
||||
Name: message.Attributes.Name,
|
||||
DeviceType: DeviceType(message.Type),
|
||||
VirtualType: virtualType,
|
||||
MTU: message.Attributes.MTU,
|
||||
}
|
||||
links = make([]Link, len(netlinkLinks))
|
||||
for i := range netlinkLinks {
|
||||
links[i] = netlinkLinkToLink(netlinkLinks[i])
|
||||
}
|
||||
|
||||
return links, nil
|
||||
}
|
||||
|
||||
var ErrLinkNotFound = errors.New("link not found")
|
||||
|
||||
func (n *NetLink) LinkByName(name string) (link Link, err error) {
|
||||
links, err := n.LinkList()
|
||||
netlinkLink, err := netlink.LinkByName(name)
|
||||
if err != nil {
|
||||
return Link{}, fmt.Errorf("listing links: %w", err)
|
||||
return Link{}, err
|
||||
}
|
||||
|
||||
for _, link := range links {
|
||||
if link.Name == name {
|
||||
return link, nil
|
||||
}
|
||||
}
|
||||
|
||||
return Link{}, fmt.Errorf("%w: for name %s", ErrLinkNotFound, name)
|
||||
return netlinkLinkToLink(netlinkLink), nil
|
||||
}
|
||||
|
||||
func (n *NetLink) LinkByIndex(index uint32) (link Link, err error) {
|
||||
links, err := n.LinkList()
|
||||
func (n *NetLink) LinkByIndex(index int) (link Link, err error) {
|
||||
netlinkLink, err := netlink.LinkByIndex(index)
|
||||
if err != nil {
|
||||
return Link{}, fmt.Errorf("listing links: %w", err)
|
||||
return Link{}, err
|
||||
}
|
||||
|
||||
for _, link = range links {
|
||||
if link.Index == index {
|
||||
return link, nil
|
||||
}
|
||||
}
|
||||
|
||||
return Link{}, fmt.Errorf("%w: for index %d", ErrLinkNotFound, index)
|
||||
return netlinkLinkToLink(netlinkLink), nil
|
||||
}
|
||||
|
||||
func (n *NetLink) LinkAdd(link Link) (linkIndex uint32, err error) {
|
||||
conn, err := rtnetlink.Dial(nil)
|
||||
func (n *NetLink) LinkAdd(link Link) (linkIndex int, err error) {
|
||||
netlinkLink := linkToNetlinkLink(&link)
|
||||
err = netlink.LinkAdd(netlinkLink)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("dialing netlink: %w", err)
|
||||
return 0, err
|
||||
}
|
||||
defer conn.Close()
|
||||
return netlinkLink.Attrs().Index, nil
|
||||
}
|
||||
|
||||
tx := &rtnetlink.LinkMessage{
|
||||
Type: uint16(link.DeviceType),
|
||||
Attributes: &rtnetlink.LinkAttributes{
|
||||
MTU: link.MTU,
|
||||
Name: link.Name,
|
||||
func (n *NetLink) LinkDel(link Link) (err error) {
|
||||
return netlink.LinkDel(linkToNetlinkLink(&link))
|
||||
}
|
||||
|
||||
func (n *NetLink) LinkSetUp(link Link) (linkIndex int, err error) {
|
||||
netlinkLink := linkToNetlinkLink(&link)
|
||||
err = netlink.LinkSetUp(netlinkLink)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return netlinkLink.Attrs().Index, nil
|
||||
}
|
||||
|
||||
func (n *NetLink) LinkSetDown(link Link) (err error) {
|
||||
return netlink.LinkSetDown(linkToNetlinkLink(&link))
|
||||
}
|
||||
|
||||
type netlinkLinkImpl struct {
|
||||
attrs *netlink.LinkAttrs
|
||||
linkType string
|
||||
}
|
||||
|
||||
func (n *netlinkLinkImpl) Attrs() *netlink.LinkAttrs {
|
||||
return n.attrs
|
||||
}
|
||||
|
||||
func (n *netlinkLinkImpl) Type() string {
|
||||
return n.linkType
|
||||
}
|
||||
|
||||
func netlinkLinkToLink(netlinkLink netlink.Link) Link {
|
||||
attributes := netlinkLink.Attrs()
|
||||
return Link{
|
||||
Type: netlinkLink.Type(),
|
||||
Name: attributes.Name,
|
||||
Index: attributes.Index,
|
||||
EncapType: attributes.EncapType,
|
||||
MTU: uint16(attributes.MTU), //nolint:gosec
|
||||
}
|
||||
}
|
||||
|
||||
// Warning: we must return `netlink.Link` and not `netlinkLinkImpl`
|
||||
// so that the vishvananda/netlink package can compare the returned
|
||||
// value against an untyped nil.
|
||||
func linkToNetlinkLink(link *Link) netlink.Link {
|
||||
if link == nil {
|
||||
return nil
|
||||
}
|
||||
return &netlinkLinkImpl{
|
||||
linkType: link.Type,
|
||||
attrs: &netlink.LinkAttrs{
|
||||
Name: link.Name,
|
||||
Index: link.Index,
|
||||
EncapType: link.EncapType,
|
||||
MTU: int(link.MTU),
|
||||
},
|
||||
}
|
||||
if link.VirtualType != "" {
|
||||
tx.Attributes.Info = &rtnetlink.LinkInfo{
|
||||
Kind: link.VirtualType,
|
||||
}
|
||||
}
|
||||
|
||||
err = conn.Link.New(tx)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("creating new link: %w", err)
|
||||
}
|
||||
|
||||
linkMessages, err := conn.Link.List()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("listing links: %w", err)
|
||||
}
|
||||
for _, linkMessage := range linkMessages {
|
||||
if linkMessage.Attributes.Name == link.Name {
|
||||
return linkMessage.Index, nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("%w: matching name %s", ErrLinkNotFound, link.Name)
|
||||
}
|
||||
|
||||
func (n *NetLink) LinkDel(linkIndex uint32) (err error) {
|
||||
conn, err := rtnetlink.Dial(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dialing netlink: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
return conn.Link.Delete(linkIndex)
|
||||
}
|
||||
|
||||
func (n *NetLink) LinkSetUp(linkIndex uint32) (err error) {
|
||||
conn, err := rtnetlink.Dial(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dialing netlink: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
rx, err := conn.Link.Get(linkIndex)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting link: %w", err)
|
||||
}
|
||||
tx := &rtnetlink.LinkMessage{
|
||||
Type: rx.Type,
|
||||
Index: linkIndex,
|
||||
Flags: iffUp,
|
||||
Change: iffUp,
|
||||
}
|
||||
return conn.Link.Set(tx)
|
||||
}
|
||||
|
||||
func (n *NetLink) LinkSetDown(linkIndex uint32) (err error) {
|
||||
conn, err := rtnetlink.Dial(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dialing netlink: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
linkInfo, err := conn.Link.Get(linkIndex)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting link: %w", err)
|
||||
}
|
||||
message := &rtnetlink.LinkMessage{
|
||||
Type: linkInfo.Type,
|
||||
Index: linkIndex,
|
||||
Flags: 0,
|
||||
Change: iffUp,
|
||||
}
|
||||
return conn.Link.Set(message)
|
||||
}
|
||||
|
||||
func (n *NetLink) LinkSetMTU(linkIndex, mtu uint32) error {
|
||||
conn, err := rtnetlink.Dial(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dialing netlink: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
message := &rtnetlink.LinkMessage{
|
||||
Index: linkIndex,
|
||||
Attributes: &rtnetlink.LinkAttributes{
|
||||
MTU: mtu,
|
||||
},
|
||||
}
|
||||
|
||||
err = conn.Link.Set(message)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting MTU to %d for link at index %d: %w",
|
||||
mtu, linkIndex, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package netlink
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
const (
|
||||
DeviceTypeEthernet DeviceType = unix.ARPHRD_ETHER
|
||||
DeviceTypeLoopback DeviceType = unix.ARPHRD_LOOPBACK
|
||||
DeviceTypeNone DeviceType = unix.ARPHRD_NONE
|
||||
|
||||
iffUp = unix.IFF_UP
|
||||
)
|
||||
@@ -1,85 +0,0 @@
|
||||
//go:build linux
|
||||
|
||||
package netlink
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_NetLink_LinkList(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
netlink := &NetLink{}
|
||||
|
||||
initialLinks, err := netlink.LinkList()
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, initialLinks)
|
||||
|
||||
loopbackFound := false
|
||||
for _, link := range initialLinks {
|
||||
if link.Name != "lo" {
|
||||
continue
|
||||
}
|
||||
loopbackFound = true
|
||||
assert.Equal(t, DeviceTypeLoopback, link.DeviceType)
|
||||
break
|
||||
}
|
||||
assert.True(t, loopbackFound, "loopback interface not found")
|
||||
|
||||
testLink := Link{
|
||||
Name: makeLinkName(),
|
||||
// note if [Link.VirtualType] is set, [Link.DeviceType]
|
||||
// is ignored and gets set to [DeviceTypeNone] in LinkAdd.
|
||||
DeviceType: DeviceTypeNone,
|
||||
VirtualType: "wireguard",
|
||||
MTU: 1420,
|
||||
}
|
||||
index, err := netlink.LinkAdd(testLink)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
_ = netlink.LinkDel(index)
|
||||
})
|
||||
|
||||
links, err := netlink.LinkList()
|
||||
require.NoError(t, err)
|
||||
|
||||
testLink.Index = index
|
||||
for _, link := range links {
|
||||
if link.Name != testLink.Name {
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, testLink, link)
|
||||
return
|
||||
}
|
||||
t.Errorf("created link %q not found", testLink.Name)
|
||||
}
|
||||
|
||||
func Test_NetLink_LinkSetMTU(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
netlink := &NetLink{}
|
||||
|
||||
testLink := Link{
|
||||
Name: makeLinkName(),
|
||||
DeviceType: DeviceTypeNone,
|
||||
VirtualType: "wireguard",
|
||||
MTU: 1420,
|
||||
}
|
||||
index, err := netlink.LinkAdd(testLink)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
_ = netlink.LinkDel(index)
|
||||
})
|
||||
testLink.Index = index
|
||||
|
||||
err = netlink.LinkSetMTU(index, 1500)
|
||||
require.NoError(t, err)
|
||||
|
||||
link, err := netlink.LinkByIndex(index)
|
||||
require.NoError(t, err)
|
||||
testLink.MTU = 1500
|
||||
assert.Equal(t, testLink, link)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
//go:build !linux && !darwin
|
||||
|
||||
package netlink
|
||||
|
||||
func (n *NetLink) LinkList() (links []Link, err error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (n *NetLink) LinkByName(name string) (link Link, err error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (n *NetLink) LinkByIndex(index int) (link Link, err error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (n *NetLink) LinkAdd(link Link) (linkIndex int, err error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (n *NetLink) LinkDel(link Link) (err error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (n *NetLink) LinkSetUp(link Link) (linkIndex int, err error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (n *NetLink) LinkSetDown(link Link) (err error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
//go:build !linux
|
||||
|
||||
package netlink
|
||||
|
||||
const (
|
||||
// FamilyAll is a placeholder only and should not
|
||||
// be used.
|
||||
FamilyAll uint8 = iota
|
||||
// FamilyV4 is a placeholder only and should not
|
||||
// be used.
|
||||
FamilyV4
|
||||
// FamilyV6 is a placeholder only and should not
|
||||
// be used.
|
||||
FamilyV6
|
||||
|
||||
// DeviceTypeEthernet is a placeholder only and should not be used.
|
||||
DeviceTypeEthernet DeviceType = 0
|
||||
// DeviceTypeLoopback is a placeholder only and should not be used.
|
||||
DeviceTypeLoopback DeviceType = 0
|
||||
// DeviceTypeNone is a placeholder only and should not be used.
|
||||
DeviceTypeNone DeviceType = 0
|
||||
|
||||
// iffUp is a placeholder only and should not be used.
|
||||
iffUp = 0
|
||||
|
||||
// RouteTypeUnicast is a placeholder only and should not be used.
|
||||
RouteTypeUnicast = 0
|
||||
// ScopeUniverse is a placeholder only and should not be used.
|
||||
ScopeUniverse = 0
|
||||
// ProtoStatic is a placeholder only and should not be used.
|
||||
ProtoStatic = 0
|
||||
|
||||
// FlagInvert is a placeholder only and should not be used.
|
||||
FlagInvert = 0
|
||||
// ActionToTable is a placeholder only and should not be used.
|
||||
ActionToTable = 0
|
||||
|
||||
// rtTableCompat is a placeholder only and should not be used.
|
||||
rtTableCompat = 0
|
||||
)
|
||||
|
||||
func (n *NetLink) RuleList(family uint8) (rules []Rule, err error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (n *NetLink) RuleAdd(rule Rule) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (n *NetLink) RuleDel(rule Rule) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (n *NetLink) IsWireguardSupported() (bool, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
+48
-104
@@ -1,125 +1,69 @@
|
||||
//go:build linux || darwin
|
||||
|
||||
package netlink
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"github.com/jsimonetti/rtnetlink"
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
type Route struct {
|
||||
LinkIndex uint32
|
||||
Dst netip.Prefix
|
||||
Src netip.Prefix
|
||||
Gw netip.Addr
|
||||
Priority uint32
|
||||
Family uint8
|
||||
Table uint32
|
||||
Type uint8
|
||||
Scope uint8
|
||||
Proto uint8
|
||||
}
|
||||
func (n *NetLink) RouteList(family int) (routes []Route, err error) {
|
||||
// We set the filter to netlink.RT_FILTER_TABLE so that
|
||||
// routes from all tables are listed, as long as the filter
|
||||
// table is set to 0.
|
||||
const filterMask = netlink.RT_FILTER_TABLE
|
||||
// The filter is not left to `nil` otherwise non-main tables
|
||||
// are ignored.
|
||||
filter := &netlink.Route{}
|
||||
|
||||
func (r *Route) fromMessage(message rtnetlink.RouteMessage) {
|
||||
table := uint32(message.Table)
|
||||
if table == 0 || table == rtTableCompat {
|
||||
table = message.Attributes.Table
|
||||
}
|
||||
r.LinkIndex = message.Attributes.OutIface
|
||||
r.Dst = ipAndLengthToPrefix(&message.Attributes.Dst, message.DstLength)
|
||||
r.Src = ipAndLengthToPrefix(&message.Attributes.Src, message.SrcLength)
|
||||
r.Gw = netIPToNetipAddress(message.Attributes.Gateway)
|
||||
r.Priority = message.Attributes.Priority
|
||||
r.Family = message.Family
|
||||
r.Table = table
|
||||
r.Type = message.Type
|
||||
r.Scope = message.Scope
|
||||
r.Proto = message.Protocol
|
||||
}
|
||||
|
||||
func (r Route) message() *rtnetlink.RouteMessage {
|
||||
dst, dstLength := prefixToIPAndLength(r.Dst)
|
||||
src, srcLength := prefixToIPAndLength(r.Src)
|
||||
var table uint8
|
||||
var extendedTable uint32
|
||||
if r.Table <= uint32(^uint8(0)) {
|
||||
table = uint8(r.Table)
|
||||
} else {
|
||||
table = rtTableCompat
|
||||
extendedTable = r.Table
|
||||
}
|
||||
message := &rtnetlink.RouteMessage{
|
||||
Family: r.Family,
|
||||
DstLength: dstLength,
|
||||
SrcLength: srcLength,
|
||||
Table: table,
|
||||
Type: r.Type,
|
||||
Scope: r.Scope,
|
||||
Protocol: r.Proto,
|
||||
Attributes: rtnetlink.RouteAttributes{
|
||||
OutIface: r.LinkIndex,
|
||||
Dst: *dst, // there should always be a dst for routes
|
||||
Gateway: netipAddrToNetIP(r.Gw),
|
||||
Priority: r.Priority,
|
||||
Table: extendedTable,
|
||||
},
|
||||
}
|
||||
if src != nil { // src is optional
|
||||
message.Attributes.Src = *src
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
func (n *NetLink) RouteList(family uint8) (routes []Route, err error) {
|
||||
conn, err := rtnetlink.Dial(nil)
|
||||
netlinkRoutes, err := netlink.RouteListFiltered(family, filter, filterMask)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dialing netlink: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
routeMessages, err := conn.Route.List()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listing interfaces: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
routes = make([]Route, 0, len(routeMessages))
|
||||
for _, routeMessage := range routeMessages {
|
||||
if family != FamilyAll && routeMessage.Family != family {
|
||||
continue
|
||||
}
|
||||
var route Route
|
||||
route.fromMessage(routeMessage)
|
||||
routes = append(routes, route)
|
||||
routes = make([]Route, len(netlinkRoutes))
|
||||
for i := range netlinkRoutes {
|
||||
routes[i] = netlinkRouteToRoute(netlinkRoutes[i])
|
||||
}
|
||||
return routes, nil
|
||||
}
|
||||
|
||||
func (n *NetLink) RouteAdd(route Route) error {
|
||||
conn, err := rtnetlink.Dial(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dialing netlink: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
return conn.Route.Add(route.message())
|
||||
netlinkRoute := routeToNetlinkRoute(route)
|
||||
return netlink.RouteAdd(&netlinkRoute)
|
||||
}
|
||||
|
||||
func (n *NetLink) RouteDel(route Route) error {
|
||||
conn, err := rtnetlink.Dial(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dialing netlink: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
return conn.Route.Delete(route.message())
|
||||
netlinkRoute := routeToNetlinkRoute(route)
|
||||
return netlink.RouteDel(&netlinkRoute)
|
||||
}
|
||||
|
||||
func (n *NetLink) RouteReplace(route Route) error {
|
||||
conn, err := rtnetlink.Dial(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dialing netlink: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
return conn.Route.Replace(route.message())
|
||||
netlinkRoute := routeToNetlinkRoute(route)
|
||||
return netlink.RouteReplace(&netlinkRoute)
|
||||
}
|
||||
|
||||
func netlinkRouteToRoute(netlinkRoute netlink.Route) (route Route) {
|
||||
return Route{
|
||||
LinkIndex: netlinkRoute.LinkIndex,
|
||||
Dst: netIPNetToNetipPrefix(netlinkRoute.Dst),
|
||||
Src: netIPToNetipAddress(netlinkRoute.Src),
|
||||
Gw: netIPToNetipAddress(netlinkRoute.Gw),
|
||||
Priority: netlinkRoute.Priority,
|
||||
Family: netlinkRoute.Family,
|
||||
Table: netlinkRoute.Table,
|
||||
Type: netlinkRoute.Type,
|
||||
}
|
||||
}
|
||||
|
||||
func routeToNetlinkRoute(route Route) (netlinkRoute netlink.Route) {
|
||||
return netlink.Route{
|
||||
LinkIndex: route.LinkIndex,
|
||||
Dst: netipPrefixToIPNet(route.Dst),
|
||||
Src: netipAddrToNetIP(route.Src),
|
||||
Gw: netipAddrToNetIP(route.Gw),
|
||||
Priority: route.Priority,
|
||||
Family: route.Family,
|
||||
Table: route.Table,
|
||||
Type: route.Type,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package netlink
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
const (
|
||||
RouteTypeUnicast = unix.RTN_UNICAST
|
||||
ScopeUniverse = unix.RT_SCOPE_UNIVERSE
|
||||
ProtoStatic = unix.RTPROT_STATIC
|
||||
|
||||
rtTableCompat = unix.RT_TABLE_COMPAT
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
//go:build !linux && !darwin
|
||||
|
||||
package netlink
|
||||
|
||||
func (n *NetLink) RouteList(family int) (
|
||||
routes []Route, err error,
|
||||
) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (n *NetLink) RouteAdd(route Route) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (n *NetLink) RouteDel(route Route) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (n *NetLink) RouteReplace(route Route) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
+73
-78
@@ -1,96 +1,91 @@
|
||||
//go:build linux
|
||||
|
||||
package netlink
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"github.com/jsimonetti/rtnetlink"
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
type Rule struct {
|
||||
Priority *uint32
|
||||
Family uint8
|
||||
Table uint32
|
||||
Mark *uint32
|
||||
Src netip.Prefix
|
||||
Dst netip.Prefix
|
||||
Flags uint32
|
||||
Action uint8
|
||||
func NewRule() Rule {
|
||||
// defaults found from netlink.NewRule() for fields we use,
|
||||
// the rest of the defaults is set when converting from a `Rule`
|
||||
// to a `netlink.Rule`
|
||||
return Rule{
|
||||
Priority: -1,
|
||||
Mark: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Rule) fromMessage(message rtnetlink.RuleMessage) {
|
||||
table := uint32(message.Table)
|
||||
if table == 0 || table == rtTableCompat {
|
||||
table = *message.Attributes.Table
|
||||
func (n *NetLink) RuleList(family int) (rules []Rule, err error) {
|
||||
switch family {
|
||||
case FamilyAll:
|
||||
n.debugLogger.Debug("ip -4 rule list")
|
||||
n.debugLogger.Debug("ip -6 rule list")
|
||||
case FamilyV4:
|
||||
n.debugLogger.Debug("ip -4 rule list")
|
||||
case FamilyV6:
|
||||
n.debugLogger.Debug("ip -6 rule list")
|
||||
}
|
||||
r.Priority = message.Attributes.Priority
|
||||
r.Family = message.Family
|
||||
r.Table = table
|
||||
r.Mark = message.Attributes.FwMark
|
||||
r.Src = ipAndLengthToPrefix(message.Attributes.Src, message.SrcLength)
|
||||
r.Dst = ipAndLengthToPrefix(message.Attributes.Dst, message.DstLength)
|
||||
r.Flags = message.Flags
|
||||
r.Action = message.Action
|
||||
netlinkRules, err := netlink.RuleList(family)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rules = make([]Rule, len(netlinkRules))
|
||||
for i := range netlinkRules {
|
||||
rules[i] = netlinkRuleToRule(netlinkRules[i])
|
||||
}
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func (r Rule) message() *rtnetlink.RuleMessage {
|
||||
src, srcLength := prefixToIPAndLength(r.Src)
|
||||
dst, dstLength := prefixToIPAndLength(r.Dst)
|
||||
|
||||
message := &rtnetlink.RuleMessage{
|
||||
Family: r.Family,
|
||||
SrcLength: srcLength,
|
||||
DstLength: dstLength,
|
||||
Flags: r.Flags,
|
||||
Action: r.Action,
|
||||
Attributes: &rtnetlink.RuleAttributes{
|
||||
Priority: r.Priority,
|
||||
FwMark: r.Mark,
|
||||
Src: src,
|
||||
Dst: dst,
|
||||
},
|
||||
}
|
||||
|
||||
if r.Table <= uint32(^uint8(0)) {
|
||||
message.Table = uint8(r.Table)
|
||||
} else {
|
||||
message.Table = rtTableCompat
|
||||
message.Attributes.Table = &r.Table
|
||||
}
|
||||
|
||||
return message
|
||||
func (n *NetLink) RuleAdd(rule Rule) error {
|
||||
n.debugLogger.Debug(ruleDbgMsg(true, rule))
|
||||
netlinkRule := ruleToNetlinkRule(rule)
|
||||
return netlink.RuleAdd(&netlinkRule)
|
||||
}
|
||||
|
||||
func (r Rule) String() string {
|
||||
from := "all"
|
||||
if r.Src.IsValid() && !r.Src.Addr().IsUnspecified() {
|
||||
from = r.Src.String()
|
||||
}
|
||||
|
||||
to := "all"
|
||||
if r.Dst.IsValid() && !r.Dst.Addr().IsUnspecified() {
|
||||
to = r.Dst.String()
|
||||
}
|
||||
|
||||
priority := ""
|
||||
if r.Priority != nil {
|
||||
priority = fmt.Sprintf(" %d", *r.Priority)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("ip rule%s: from %s to %s table %d",
|
||||
priority, from, to, r.Table)
|
||||
func (n *NetLink) RuleDel(rule Rule) error {
|
||||
n.debugLogger.Debug(ruleDbgMsg(false, rule))
|
||||
netlinkRule := ruleToNetlinkRule(rule)
|
||||
return netlink.RuleDel(&netlinkRule)
|
||||
}
|
||||
|
||||
func (r Rule) debugMessage(add bool) (debugMessage string) {
|
||||
func ruleToNetlinkRule(rule Rule) (netlinkRule netlink.Rule) {
|
||||
netlinkRule = *netlink.NewRule()
|
||||
netlinkRule.Priority = rule.Priority
|
||||
netlinkRule.Family = rule.Family
|
||||
netlinkRule.Table = rule.Table
|
||||
netlinkRule.Mark = rule.Mark
|
||||
netlinkRule.Src = netipPrefixToIPNet(rule.Src)
|
||||
netlinkRule.Dst = netipPrefixToIPNet(rule.Dst)
|
||||
netlinkRule.Invert = rule.Invert
|
||||
return netlinkRule
|
||||
}
|
||||
|
||||
func netlinkRuleToRule(netlinkRule netlink.Rule) (rule Rule) {
|
||||
return Rule{
|
||||
Priority: netlinkRule.Priority,
|
||||
Family: netlinkRule.Family,
|
||||
Table: netlinkRule.Table,
|
||||
Mark: netlinkRule.Mark,
|
||||
Src: netIPNetToNetipPrefix(netlinkRule.Src),
|
||||
Dst: netIPNetToNetipPrefix(netlinkRule.Dst),
|
||||
Invert: netlinkRule.Invert,
|
||||
}
|
||||
}
|
||||
|
||||
func ruleDbgMsg(add bool, rule Rule) (debugMessage string) {
|
||||
debugMessage = "ip"
|
||||
|
||||
switch r.Family {
|
||||
switch rule.Family {
|
||||
case FamilyV4:
|
||||
debugMessage += " -f inet"
|
||||
case FamilyV6:
|
||||
debugMessage += " -f inet6"
|
||||
default:
|
||||
debugMessage += " -f " + fmt.Sprint(r.Family)
|
||||
debugMessage += " -f " + fmt.Sprint(rule.Family)
|
||||
}
|
||||
|
||||
debugMessage += " rule"
|
||||
@@ -101,20 +96,20 @@ func (r Rule) debugMessage(add bool) (debugMessage string) {
|
||||
debugMessage += " del"
|
||||
}
|
||||
|
||||
if r.Src.IsValid() {
|
||||
debugMessage += " from " + r.Src.String()
|
||||
if rule.Src.IsValid() {
|
||||
debugMessage += " from " + rule.Src.String()
|
||||
}
|
||||
|
||||
if r.Dst.IsValid() {
|
||||
debugMessage += " to " + r.Dst.String()
|
||||
if rule.Dst.IsValid() {
|
||||
debugMessage += " to " + rule.Dst.String()
|
||||
}
|
||||
|
||||
if r.Table != 0 {
|
||||
debugMessage += " lookup " + fmt.Sprint(r.Table)
|
||||
if rule.Table != 0 {
|
||||
debugMessage += " lookup " + fmt.Sprint(rule.Table)
|
||||
}
|
||||
|
||||
if r.Priority != nil {
|
||||
debugMessage += " pref " + fmt.Sprint(*r.Priority)
|
||||
if rule.Priority != -1 {
|
||||
debugMessage += " pref " + fmt.Sprint(rule.Priority)
|
||||
}
|
||||
|
||||
return debugMessage
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
package netlink
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jsimonetti/rtnetlink"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
FlagInvert = unix.FIB_RULE_INVERT
|
||||
ActionToTable = unix.FR_ACT_TO_TBL
|
||||
)
|
||||
|
||||
func (n *NetLink) RuleList(family uint8) (rules []Rule, err error) {
|
||||
switch family {
|
||||
case FamilyAll:
|
||||
n.debugLogger.Debug("ip -4 rule list")
|
||||
n.debugLogger.Debug("ip -6 rule list")
|
||||
case FamilyV4:
|
||||
n.debugLogger.Debug("ip -4 rule list")
|
||||
case FamilyV6:
|
||||
n.debugLogger.Debug("ip -6 rule list")
|
||||
}
|
||||
|
||||
conn, err := rtnetlink.Dial(nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dialing netlink: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
ruleMessages, err := conn.Rule.List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rules = make([]Rule, 0, len(ruleMessages))
|
||||
for _, message := range ruleMessages {
|
||||
if family != FamilyAll && family != message.Family {
|
||||
continue
|
||||
}
|
||||
var rule Rule
|
||||
rule.fromMessage(message)
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func (n *NetLink) RuleAdd(rule Rule) error {
|
||||
n.debugLogger.Debug(rule.debugMessage(true))
|
||||
|
||||
conn, err := rtnetlink.Dial(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dialing netlink: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
return conn.Rule.Add(rule.message())
|
||||
}
|
||||
|
||||
func (n *NetLink) RuleDel(rule Rule) error {
|
||||
n.debugLogger.Debug(rule.debugMessage(false))
|
||||
|
||||
conn, err := rtnetlink.Dial(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dialing netlink: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
return conn.Rule.Delete(rule.message())
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Rule_debugMessage(t *testing.T) {
|
||||
func Test_ruleDbgMsg(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
@@ -15,7 +15,7 @@ func Test_Rule_debugMessage(t *testing.T) {
|
||||
dbgMsg string
|
||||
}{
|
||||
"default values": {
|
||||
dbgMsg: "ip -f 0 rule del",
|
||||
dbgMsg: "ip -f 0 rule del pref 0",
|
||||
},
|
||||
"add rule": {
|
||||
add: true,
|
||||
@@ -24,7 +24,7 @@ func Test_Rule_debugMessage(t *testing.T) {
|
||||
Src: makeNetipPrefix(1),
|
||||
Dst: makeNetipPrefix(2),
|
||||
Table: 100,
|
||||
Priority: ptrTo(uint32(101)),
|
||||
Priority: 101,
|
||||
},
|
||||
dbgMsg: "ip -f inet rule add from 1.1.1.0/24 to 2.2.2.0/24 lookup 100 pref 101",
|
||||
},
|
||||
@@ -34,7 +34,7 @@ func Test_Rule_debugMessage(t *testing.T) {
|
||||
Src: makeNetipPrefix(1),
|
||||
Dst: makeNetipPrefix(2),
|
||||
Table: 100,
|
||||
Priority: ptrTo(uint32(101)),
|
||||
Priority: 101,
|
||||
},
|
||||
dbgMsg: "ip -f inet rule del from 1.1.1.0/24 to 2.2.2.0/24 lookup 100 pref 101",
|
||||
},
|
||||
@@ -44,7 +44,7 @@ func Test_Rule_debugMessage(t *testing.T) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbgMsg := testCase.rule.debugMessage(testCase.add)
|
||||
dbgMsg := ruleDbgMsg(testCase.add, testCase.rule)
|
||||
|
||||
assert.Equal(t, testCase.dbgMsg, dbgMsg)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
//go:build !linux
|
||||
|
||||
package netlink
|
||||
|
||||
func NewRule() Rule {
|
||||
return Rule{}
|
||||
}
|
||||
|
||||
func (n *NetLink) RuleList(family int) (rules []Rule, err error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (n *NetLink) RuleAdd(rule Rule) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (n *NetLink) RuleDel(rule Rule) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package netlink
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
type Addr struct {
|
||||
Network netip.Prefix
|
||||
}
|
||||
|
||||
func (a Addr) String() string {
|
||||
return a.Network.String()
|
||||
}
|
||||
|
||||
type Link struct {
|
||||
Type string
|
||||
Name string
|
||||
Index int
|
||||
EncapType string
|
||||
MTU uint16
|
||||
}
|
||||
|
||||
type Route struct {
|
||||
LinkIndex int
|
||||
Dst netip.Prefix
|
||||
Src netip.Addr
|
||||
Gw netip.Addr
|
||||
Priority int
|
||||
Family int
|
||||
Table int
|
||||
Type int
|
||||
}
|
||||
|
||||
type Rule struct {
|
||||
Priority int
|
||||
Family int
|
||||
Table int
|
||||
Mark uint32
|
||||
Src netip.Prefix
|
||||
Dst netip.Prefix
|
||||
Invert bool
|
||||
}
|
||||
|
||||
func (r Rule) String() string {
|
||||
from := "all"
|
||||
if r.Src.IsValid() && !r.Src.Addr().IsUnspecified() {
|
||||
from = r.Src.String()
|
||||
}
|
||||
|
||||
to := "all"
|
||||
if r.Dst.IsValid() && !r.Dst.Addr().IsUnspecified() {
|
||||
to = r.Dst.String()
|
||||
}
|
||||
|
||||
return fmt.Sprintf("ip rule %d: from %s to %s table %d",
|
||||
r.Priority, from, to, r.Table)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
//go:build linux
|
||||
|
||||
package netlink
|
||||
|
||||
import (
|
||||
"github.com/qdm12/gluetun/internal/mod"
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
func (n *NetLink) IsWireguardSupported() bool {
|
||||
// Check for Wireguard family without loading the wireguard module.
|
||||
// Some kernels have the wireguard module built-in, and don't have a
|
||||
// modules directory, such as WSL2 kernels.
|
||||
ok := hasWireguardFamily()
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
|
||||
// Try loading the wireguard module, since some systems do not load
|
||||
// it after a boot. If this fails, wireguard is assumed to not be supported.
|
||||
n.debugLogger.Debugf("wireguard family not found, trying to load wireguard kernel module")
|
||||
err := mod.Probe("wireguard")
|
||||
if err != nil {
|
||||
n.debugLogger.Debugf("failed loading wireguard kernel module: %s", err)
|
||||
return false
|
||||
}
|
||||
n.debugLogger.Debugf("wireguard kernel module loaded successfully")
|
||||
|
||||
// Re-check if the Wireguard family is now available, after loading
|
||||
// the wireguard kernel module.
|
||||
return hasWireguardFamily()
|
||||
}
|
||||
|
||||
func hasWireguardFamily() bool {
|
||||
_, err := netlink.GenlFamilyGet("wireguard")
|
||||
return err == nil
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package netlink
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/mdlayher/genetlink"
|
||||
"github.com/qdm12/gluetun/internal/mod"
|
||||
)
|
||||
|
||||
func (n *NetLink) IsWireguardSupported() (ok bool, err error) {
|
||||
// Check for Wireguard family without loading the wireguard module.
|
||||
// Some kernels have the wireguard module built-in, and don't have a
|
||||
// modules directory, such as WSL2 kernels.
|
||||
ok, err = hasWireguardFamily()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("checking wireguard family: %w", err)
|
||||
} else if ok {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Try loading the wireguard module, since some systems do not load
|
||||
// it after a boot. If this fails, wireguard is assumed to not be supported.
|
||||
n.debugLogger.Debugf("wireguard family not found, trying to load wireguard kernel module")
|
||||
err = mod.Probe("wireguard")
|
||||
if err != nil {
|
||||
n.debugLogger.Debugf("failed loading wireguard kernel module: %s", err)
|
||||
return false, nil
|
||||
}
|
||||
n.debugLogger.Debugf("wireguard kernel module loaded successfully")
|
||||
|
||||
// Re-check if the Wireguard family is now available, after loading
|
||||
// the wireguard kernel module.
|
||||
ok, err = hasWireguardFamily()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("checking wireguard family: %w", err)
|
||||
}
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
func hasWireguardFamily() (ok bool, err error) {
|
||||
conn, err := genetlink.Dial(nil)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("dialing netlink: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_, err = conn.GetFamily("wireguard")
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("getting wireguard family: %w", err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@@ -4,8 +4,6 @@ package netlink
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_NetLink_IsWireguardSupported(t *testing.T) {
|
||||
@@ -14,8 +12,7 @@ func Test_NetLink_IsWireguardSupported(t *testing.T) {
|
||||
netLink := &NetLink{
|
||||
debugLogger: &noopLogger{},
|
||||
}
|
||||
ok, err := netLink.IsWireguardSupported()
|
||||
require.NoError(t, err)
|
||||
ok := netLink.IsWireguardSupported()
|
||||
if ok { // cannot assert since this depends on kernel
|
||||
t.Log("wireguard is supported")
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build !linux
|
||||
|
||||
package netlink
|
||||
|
||||
func (n *NetLink) IsWireguardSupported() (ok bool, err error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
@@ -55,6 +55,8 @@ func extractDataFromLines(lines []string) (
|
||||
func extractDataFromLine(line string) (
|
||||
ip netip.Addr, port uint16, protocol string, err error,
|
||||
) {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(line, "proto "):
|
||||
protocol, err = extractProto(line)
|
||||
@@ -81,10 +83,7 @@ func extractDataFromLine(line string) (
|
||||
return ip, 0, "", nil
|
||||
}
|
||||
|
||||
var (
|
||||
errProtoLineFieldsCount = errors.New("proto line has not 2 fields as expected")
|
||||
errProtocolNotSupported = errors.New("network protocol not supported")
|
||||
)
|
||||
var errProtoLineFieldsCount = errors.New("proto line has not 2 fields as expected")
|
||||
|
||||
func extractProto(line string) (protocol string, err error) {
|
||||
fields := strings.Fields(line)
|
||||
@@ -92,13 +91,25 @@ func extractProto(line string) (protocol string, err error) {
|
||||
return "", fmt.Errorf("%w: %s", errProtoLineFieldsCount, line)
|
||||
}
|
||||
|
||||
switch fields[1] {
|
||||
case "tcp", "tcp4", "tcp6", "tcp-client", "udp", "udp4", "udp6":
|
||||
default:
|
||||
return "", fmt.Errorf("%w: %s", errProtocolNotSupported, fields[1])
|
||||
}
|
||||
return parseProto(fields[1])
|
||||
}
|
||||
|
||||
return fields[1], nil
|
||||
var errProtocolNotSupported = errors.New("network protocol not supported")
|
||||
|
||||
func parseProto(field string) (protocol string, err error) {
|
||||
switch field {
|
||||
case "tcp", "tcp4", "tcp6", "tcp-client":
|
||||
// tcp4, tcp6 can be assimilated as tcp since the IP version is
|
||||
// determined by the remote IP address version.
|
||||
// tcp-client is a synonym of tcp for OpenVPN 2.5+ acting in client mode.
|
||||
return constants.TCP, nil
|
||||
case "udp", "udp4", "udp6":
|
||||
// udp4, udp6 can be assimilated as udp since the IP version is
|
||||
// determined by the remote IP address version.
|
||||
return constants.UDP, nil
|
||||
default:
|
||||
return "", fmt.Errorf("%w: %s", errProtocolNotSupported, field)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -136,11 +147,9 @@ func extractRemote(line string) (ip netip.Addr, port uint16,
|
||||
}
|
||||
|
||||
if n > 3 { //nolint:mnd
|
||||
switch fields[3] {
|
||||
case "tcp", "udp":
|
||||
protocol = fields[3]
|
||||
default:
|
||||
return netip.Addr{}, 0, "", fmt.Errorf("%w: %s", errProtocolNotSupported, fields[3])
|
||||
protocol, err = parseProto(fields[3])
|
||||
if err != nil {
|
||||
return netip.Addr{}, 0, "", fmt.Errorf("parsing protocol from remote line: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,14 @@ func Test_extractDataFromLines(t *testing.T) {
|
||||
Protocol: constants.UDP,
|
||||
},
|
||||
},
|
||||
"leading_whitespace": {
|
||||
lines: []string{" proto tcp", "\tremote 1.2.3.4 443 tcp"},
|
||||
connection: models.Connection{
|
||||
IP: netip.AddrFrom4([4]byte{1, 2, 3, 4}),
|
||||
Port: 443,
|
||||
Protocol: constants.TCP,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
@@ -105,7 +113,7 @@ func Test_extractDataFromLine(t *testing.T) {
|
||||
},
|
||||
"tcp-client": {
|
||||
line: "proto tcp-client",
|
||||
protocol: "tcp-client",
|
||||
protocol: constants.TCP,
|
||||
},
|
||||
"extract remote error": {
|
||||
line: "remote bad",
|
||||
@@ -239,7 +247,7 @@ func Test_extractRemote(t *testing.T) {
|
||||
},
|
||||
"invalid protocol": {
|
||||
line: "remote 1.2.3.4 8000 bad",
|
||||
err: errors.New("network protocol not supported: bad"),
|
||||
err: errors.New("parsing protocol from remote line: network protocol not supported: bad"),
|
||||
},
|
||||
"IP host and port and protocol": {
|
||||
line: "remote 1.2.3.4 8000 udp",
|
||||
|
||||
@@ -29,19 +29,16 @@ func (r *Runner) Run(ctx context.Context, errCh chan<- error, ready chan<- struc
|
||||
return
|
||||
}
|
||||
|
||||
streamCtx, streamCancel := context.WithCancel(context.Background())
|
||||
streamDone := make(chan struct{})
|
||||
go streamLines(streamCtx, streamDone, r.logger,
|
||||
go streamLines(streamDone, r.logger,
|
||||
stdoutLines, stderrLines, ready)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
<-waitError
|
||||
streamCancel()
|
||||
<-streamDone
|
||||
errCh <- ctx.Err()
|
||||
case err := <-waitError:
|
||||
streamCancel()
|
||||
<-streamDone
|
||||
errCh <- err
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants/openvpn"
|
||||
)
|
||||
@@ -32,7 +33,7 @@ func start(ctx context.Context, starter CmdStarter, version string, flags []stri
|
||||
args := []string{"--config", configPath}
|
||||
args = append(args, flags...)
|
||||
cmd := exec.CommandContext(ctx, bin, args...)
|
||||
setCmdSysProcAttr(cmd)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
|
||||
return starter.Start(cmd)
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package openvpn
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func setCmdSysProcAttr(cmd *exec.Cmd) {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
//go:build !linux
|
||||
|
||||
package openvpn
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func setCmdSysProcAttr(cmd *exec.Cmd) {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
||||
}
|
||||
@@ -1,26 +1,37 @@
|
||||
package openvpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func streamLines(ctx context.Context, done chan<- struct{},
|
||||
func streamLines(done chan<- struct{},
|
||||
logger Logger, stdout, stderr <-chan string,
|
||||
tunnelReady chan<- struct{},
|
||||
) {
|
||||
defer close(done)
|
||||
|
||||
var line string
|
||||
|
||||
for {
|
||||
var line string
|
||||
var ok bool
|
||||
errLine := false
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case line = <-stdout:
|
||||
case line = <-stderr:
|
||||
errLine = true
|
||||
case line, ok = <-stdout:
|
||||
if ok {
|
||||
break
|
||||
}
|
||||
if stderr == nil {
|
||||
return
|
||||
}
|
||||
stdout = nil
|
||||
case line, ok = <-stderr:
|
||||
if ok {
|
||||
errLine = true
|
||||
break
|
||||
}
|
||||
if stdout == nil {
|
||||
return
|
||||
}
|
||||
stderr = nil
|
||||
}
|
||||
line, level := processLogLine(line)
|
||||
if line == "" {
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
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 uint32) (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
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
//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
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
//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)
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package pmtud
|
||||
|
||||
type Logger interface {
|
||||
Debug(msg string)
|
||||
Debugf(msg string, args ...any)
|
||||
Warnf(msg string, args ...any)
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
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 uint32 = 68
|
||||
icmpv4Protocol int = 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 uint32, pingTimeout time.Duration, logger Logger,
|
||||
) (mtu uint32, 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 = uint32(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
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 uint32, pingTimeout time.Duration, logger Logger,
|
||||
) (mtu uint32, 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 = uint32(typedBody.MTU) //nolint:gosec
|
||||
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 uint32(typedBody.MTU), nil //nolint:gosec
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
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 uint32) (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 uint32
|
||||
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
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package pmtud
|
||||
|
||||
type noopLogger struct{}
|
||||
|
||||
func (noopLogger) Debug(_ string) {}
|
||||
func (noopLogger) Debugf(_ string, _ ...any) {}
|
||||
func (noopLogger) Warnf(_ string, _ ...any) {}
|
||||
@@ -1,271 +0,0 @@
|
||||
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 uint32, pingTimeout time.Duration, logger Logger) (
|
||||
mtu uint32, 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 uint32
|
||||
echoID uint16
|
||||
sentBytes int
|
||||
ok bool
|
||||
}
|
||||
|
||||
func pmtudMultiSizes(ctx context.Context, ip netip.Addr,
|
||||
minMTU, maxPossibleMTU uint32, pingTimeout time.Duration,
|
||||
logger Logger,
|
||||
) (maxMTU uint32, 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 uint32) (mtus []uint32) {
|
||||
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([]uint32, 0, diff)
|
||||
for mtu := minMTU; mtu <= maxMTU; mtu++ {
|
||||
mtus = append(mtus, mtu)
|
||||
}
|
||||
default:
|
||||
step := float64(diff) / float64(mtusLength-1)
|
||||
mtus = make([]uint32, 0, mtusLength)
|
||||
for mtu := float64(minMTU); len(mtus) < mtusLength-1; mtu += step {
|
||||
mtus = append(mtus, uint32(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
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
//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)
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package pmtud
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_makeMTUsToTest(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
minMTU uint32
|
||||
maxMTU uint32
|
||||
mtus []uint32
|
||||
}{
|
||||
"0_0": {
|
||||
mtus: []uint32{0},
|
||||
},
|
||||
"0_1": {
|
||||
maxMTU: 1,
|
||||
mtus: []uint32{0, 1},
|
||||
},
|
||||
"0_8": {
|
||||
maxMTU: 8,
|
||||
mtus: []uint32{0, 1, 2, 3, 4, 5, 6, 7, 8},
|
||||
},
|
||||
"0_12": {
|
||||
maxMTU: 12,
|
||||
mtus: []uint32{0, 1, 2, 4, 5, 6, 7, 8, 10, 11, 12},
|
||||
},
|
||||
"0_80": {
|
||||
maxMTU: 80,
|
||||
mtus: []uint32{0, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80},
|
||||
},
|
||||
"0_100": {
|
||||
maxMTU: 100,
|
||||
mtus: []uint32{0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100},
|
||||
},
|
||||
"1280_1500": {
|
||||
minMTU: 1280,
|
||||
maxMTU: 1500,
|
||||
mtus: []uint32{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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ type Service interface {
|
||||
|
||||
type Routing interface {
|
||||
VPNLocalGatewayIP(vpnInterface string) (gateway netip.Addr, err error)
|
||||
AssignedIP(interfaceName string, family uint8) (ip netip.Addr, err error)
|
||||
AssignedIP(interfaceName string, family int) (ip netip.Addr, err error)
|
||||
}
|
||||
|
||||
type PortAllower interface {
|
||||
@@ -29,6 +29,7 @@ type Logger interface {
|
||||
Info(s string)
|
||||
Warn(s string)
|
||||
Error(s string)
|
||||
Errorf(format string, args ...any)
|
||||
}
|
||||
|
||||
type Cmder interface {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"github.com/qdm12/gluetun/internal/portforward/service"
|
||||
@@ -85,6 +86,8 @@ func (l *Loop) run(runCtx context.Context, runDone chan<- struct{},
|
||||
defer close(runDone)
|
||||
|
||||
var serviceRunError <-chan error
|
||||
var retryAfter <-chan time.Time
|
||||
const retryDelay = 5 * time.Second
|
||||
for {
|
||||
updateReceived := false
|
||||
select {
|
||||
@@ -103,10 +106,12 @@ func (l *Loop) run(runCtx context.Context, runDone chan<- struct{},
|
||||
l.settingsMutex.Unlock()
|
||||
case err := <-serviceRunError:
|
||||
l.logger.Error(err.Error())
|
||||
case <-retryAfter:
|
||||
// Retry starting the service after a delay
|
||||
retryAfter = nil
|
||||
}
|
||||
|
||||
firstRun := serviceRunError == nil
|
||||
if !firstRun {
|
||||
if l.service != nil {
|
||||
err := l.service.Stop()
|
||||
if err != nil {
|
||||
runErrorCh <- fmt.Errorf("stopping previous service: %w", err)
|
||||
@@ -130,6 +135,10 @@ func (l *Loop) run(runCtx context.Context, runDone chan<- struct{},
|
||||
err = fmt.Errorf("starting port forwarding service: %w", err)
|
||||
}
|
||||
updateResult <- err
|
||||
} else if err != nil {
|
||||
// Log the error and schedule a retry
|
||||
l.logger.Errorf("starting port forwarding service: %s - retrying in %s", err, retryDelay)
|
||||
retryAfter = time.After(retryDelay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,11 @@ func runCommand(ctx context.Context, cmder Cmder, logger Logger,
|
||||
}
|
||||
portsString := strings.Join(portStrings, ",")
|
||||
commandString := strings.ReplaceAll(commandTemplate, "{{PORTS}}", portsString)
|
||||
commandString = strings.ReplaceAll(commandString, "{{PORT}}", portStrings[0])
|
||||
var firstPort string
|
||||
if len(portStrings) > 0 {
|
||||
firstPort = portStrings[0]
|
||||
}
|
||||
commandString = strings.ReplaceAll(commandString, "{{PORT}}", firstPort)
|
||||
commandString = strings.ReplaceAll(commandString, "{{VPN_INTERFACE}}", vpnInterface)
|
||||
args, err := command.Split(commandString)
|
||||
if err != nil {
|
||||
@@ -31,31 +35,39 @@ func runCommand(ctx context.Context, cmder Cmder, logger Logger,
|
||||
return err
|
||||
}
|
||||
|
||||
streamCtx, streamCancel := context.WithCancel(context.Background())
|
||||
streamDone := make(chan struct{})
|
||||
go streamLines(streamCtx, streamDone, logger, stdout, stderr)
|
||||
go streamLines(streamDone, logger, stdout, stderr)
|
||||
|
||||
err = <-waitError
|
||||
streamCancel()
|
||||
<-streamDone
|
||||
return err
|
||||
}
|
||||
|
||||
func streamLines(ctx context.Context, done chan<- struct{},
|
||||
logger Logger, stdout, stderr <-chan string,
|
||||
func streamLines(done chan<- struct{}, logger Logger,
|
||||
stdout, stderr <-chan string,
|
||||
) {
|
||||
defer close(done)
|
||||
|
||||
var line string
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case line = <-stdout:
|
||||
logger.Info(line)
|
||||
case line = <-stderr:
|
||||
logger.Error(line)
|
||||
case line, ok := <-stdout:
|
||||
if ok {
|
||||
logger.Info(line)
|
||||
break
|
||||
}
|
||||
if stderr == nil {
|
||||
return
|
||||
}
|
||||
stdout = nil
|
||||
case line, ok := <-stderr:
|
||||
if ok {
|
||||
logger.Error(line)
|
||||
break
|
||||
}
|
||||
if stdout == nil {
|
||||
return
|
||||
}
|
||||
stderr = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ type PortAllower interface {
|
||||
|
||||
type Routing interface {
|
||||
VPNLocalGatewayIP(vpnInterface string) (gateway netip.Addr, err error)
|
||||
AssignedIP(interfaceName string, family uint8) (ip netip.Addr, err error)
|
||||
AssignedIP(interfaceName string, family int) (ip netip.Addr, err error)
|
||||
}
|
||||
|
||||
type Logger interface {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gluetun/internal/constants/openvpn"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/provider/utils"
|
||||
@@ -68,7 +69,11 @@ func modifyConfig(lines []string, connection models.Connection,
|
||||
}
|
||||
|
||||
// Add values
|
||||
modified = append(modified, "proto "+connection.Protocol)
|
||||
protocol := connection.Protocol
|
||||
if protocol == constants.TCP {
|
||||
protocol = "tcp-client"
|
||||
}
|
||||
modified = append(modified, "proto "+protocol)
|
||||
modified = append(modified, fmt.Sprintf("remote %s %d", connection.IP, connection.Port))
|
||||
modified = append(modified, "dev "+settings.Interface)
|
||||
modified = append(modified, "mute-replay-warnings")
|
||||
|
||||
@@ -17,11 +17,14 @@ func (p *Provider) OpenVPNConfig(connection models.Connection,
|
||||
Ciphers: []string{
|
||||
openvpn.AES256gcm, openvpn.AES256cbc, openvpn.AES128gcm,
|
||||
},
|
||||
Auth: openvpn.SHA512,
|
||||
CAs: []string{"MIIF+DCCA+CgAwIBAgIBATANBgkqhkiG9w0BAQ0FADCBhDELMAkGA1UEBhMCVkcxDDAKBgNVBAgMA0JWSTETMBEGA1UECgwKRXhwcmVzc1ZQTjETMBEGA1UECwwKRXhwcmVzc1ZQTjEWMBQGA1UEAwwNRXhwcmVzc1ZQTiBDQTElMCMGCSqGSIb3DQEJARYWc3VwcG9ydEBleHByZXNzdnBuLmNvbTAeFw0xNTEwMjEwMDAwMDBaFw0yNjA0MDEyMTEyMDBaMIGEMQswCQYDVQQGEwJWRzEMMAoGA1UECAwDQlZJMRMwEQYDVQQKDApFeHByZXNzVlBOMRMwEQYDVQQLDApFeHByZXNzVlBOMRYwFAYDVQQDDA1FeHByZXNzVlBOIENBMSUwIwYJKoZIhvcNAQkBFhZzdXBwb3J0QGV4cHJlc3N2cG4uY29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxzXvHZ25OsESKRMQFINHJNqE9kVRLWJS50oVB2jxobudPhCsWvJSApvar8CB2RrqkVMhXu2HT3FBtDL91INg070qAyjjRpzEbDPWqQ1+G0tk0sjiJt2mXPJK2IlNFnhe6rTs09Pkpcp8qRhfZay/dIlmagohQAr4JvYL1Ajg9A3sLb8JkY03H6GhOF8EKYTqhrEppCcg4sQKQhNSytRoQAm8Ta+tnTYIedwWpqjUXP9YXFOvljPaixfYug24eAkpTjeuWTcELSyfnuiBeK+z9+5OYunhqFt2QZMq33kLFZGMN2gHRCzngxxphurypsPRo7jiFgQI1yLt8uZsEZ+otGEK91jjKfOC+g9TBy2RUtxk1neWcQ6syXDuc3rBNrGA8iM0ZoEqQ1BC8xWr3NYlSjqN+1mgpTAX3/Dxze4GzHd7AmYaYJV8xnKBVNphlMlg1giCAu5QXjMxPbfCgZiEFq/uq0SOKQJeT3AI/uVPSvwCMWByjyMbDpKKAK8Hy3UT5m4bCNu8J7bxj+vdnq0A2HPwtF0FwBl/TIM3zNsyFrZZ0j6jLRT50mFsgDBKcD4L/J5rjdCsKPu5rodhxe38rCx2GknP1Zkov4yoVCcR48+CQwg3oBkq0/EflvWUvcYApzs9SomUM/g+8Q/V0WOfJmFWuxN9YntZlnzHRSRjrvMCAwEAAaNzMHEwHQYDVR0OBBYEFIzmQGj8xS+0LLklwqHD45VVOZRJMB8GA1UdIwQYMBaAFIzmQGj8xS+0LLklwqHD45VVOZRJMA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIBFjANBgkqhkiG9w0BAQ0FAAOCAgEAbHfuMKtojm1NgX7qSU2Rm2B5L8G0FuFP0L40dj8O5WHt45j2z8coMK90vrUnQEZNQmRzot7v3XjVzVlxBWYSsCEApTsSDNi/4BNFP8H/BUUtJuy2GFTO4wDVJnqNkZOHBmyVD75s1Y+W8a+zB4jkMeDEhOHZdwQ0l1fJDDgXal5f1UT5F5WH6/RwHmWTwX4GxuCiIVtx70CjkXqhM8yZtTp1UtHLRNYcNSIes0vrAPHPgoA5z9B8UvsOjuP+mfcjzi0LGGrY+2pJu0BKO2dRnarIZZABETIisI3FokoTszx5jpRPyxyUTuRDKWHrvi0PPtOmC8nFahfugWFUi6uBsqCaSeuex+ahnTPCq0b1l0Ozpg0YeE8CW1TL9Y92b01up2c+PP6wZOIm3JyTH+L5smDFbh80V42dKyGNdPXMg5IcJhj3YfAy4k8h/qbWY57KFcIzKx40bFsoI7PeydbGtT/dIoFLSZRLW5bleXNgG9mXZp270UeEC6CpATCS6uVl8LVT1I02uulHUpFaRmTEOrmMxsXGt6UAwYTY55K/B8uuID341xKbeC0kzhuN2gsL5UJaocBHyWK/AqwbeBttdhOCLwoaj7+nSViPxICObKrg3qavGNCvtwy/fEegK9X/wlp2e2CFlIhFbadeXOBr9Fn8ypYPP17mTqe98OJYM04="}, //nolint:lll
|
||||
Cert: "MIIDTjCCAregAwIBAgIDKzZvMA0GCSqGSIb3DQEBCwUAMIGFMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFTATBgNVBAcTDFNhbkZyYW5jaXNjbzEVMBMGA1UEChMMRm9ydC1GdW5zdG9uMRgwFgYDVQQDEw9Gb3J0LUZ1bnN0b24gQ0ExITAfBgkqhkiG9w0BCQEWEm1lQG15aG9zdC5teWRvbWFpbjAgFw0xNjExMDMwMzA2MThaGA8yMDY2MTEwMzAzMDYxOFowgYoxCzAJBgNVBAYTAlZHMQwwCgYDVQQIDANCVkkxEzARBgNVBAoMCkV4cHJlc3NWUE4xEzARBgNVBAsMCkV4cHJlc3NWUE4xHDAaBgNVBAMME2V4cHJlc3N2cG5fY3VzdG9tZXIxJTAjBgkqhkiG9w0BCQEWFnN1cHBvcnRAZXhwcmVzc3Zwbi5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrOYt/KOi2uMDGev3pXg8j1SO4J/4EVWDF7vJcKr2jrZlqD/zuAFx2W1YWvwumPO6PKH4PU9621aNdiumaUkv/RplCfznnnxqobhJuTE2oA+rS1bOq+9OhHwF9jgNXNVk+XX4d0toST5uGE6Z3OdmPBur8o5AlCf78PDSAwpFOw5HrgLqOEU4hTweC1/czX2VsvsHv22HRI6JMZgP8gGQii/p9iukqfaJvGdPciL5p1QRBUQIi8P8pNvEp1pVIpxYj7/LOUqb2DxFvgmp2v1IQ0Yu88SWsFk84+xAYHzfkLyS31Sqj5uLRBnJqx3fIlOihQ50GI72fwPMwo+OippvVAgMBAAGjPzA9MAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgSwMB0GA1UdDgQWBBSkBM1TCX9kBgFsv2RmOzudMXa9njANBgkqhkiG9w0BAQsFAAOBgQA+2e4b+33zFmA+1ZQ46kWkfiB+fEeDyMwMLeYYyDS2d8mZhNZKdOw7dy4Ifz9Vqzp4aKuQ6j61c6k1UaQQL0tskqWVzslSFvs9NZyUAJLLdGUc5TT2MiLwiXQwd4UvH6bGeePdhvB4+ZbW7VMD7TE8hZhjhAL4F6yAP1EQvg3LDA==", //nolint:lll
|
||||
RSAKey: "MIIEpAIBAAKCAQEAqzmLfyjotrjAxnr96V4PI9UjuCf+BFVgxe7yXCq9o62Zag/87gBcdltWFr8Lpjzujyh+D1PettWjXYrpmlJL/0aZQn85558aqG4SbkxNqAPq0tWzqvvToR8BfY4DVzVZPl1+HdLaEk+bhhOmdznZjwbq/KOQJQn+/Dw0gMKRTsOR64C6jhFOIU8Hgtf3M19lbL7B79th0SOiTGYD/IBkIov6fYrpKn2ibxnT3Ii+adUEQVECIvD/KTbxKdaVSKcWI+/yzlKm9g8Rb4Jqdr9SENGLvPElrBZPOPsQGB835C8kt9Uqo+bi0QZyasd3yJTooUOdBiO9n8DzMKPjoqab1QIDAQABAoIBAHgsekC0SKi+AOcNOZqJ3pxqophE0V7fQX2KWGXhxZnUZMFxGTc936deMYzjZ1y0lUa6x8cgOUcfqHol3hDmw9oWBckLHGv5Wi9umdb6DOLoZO62+FQATSdfaJ9jheq2Ub2YxsRN0apaXzB6KDKz0oM0+sZ4Udn9Kw6DfuIELRIWwEx4w0v3gKW7YLC4Jkc4AwLkPK03xEA/qImfkCmaMPLhrgVQt+IFfP8bXzL7CCC04rNU/IS8pyjex+iUolnQZlbXntF7Bm4V2mz0827ZVqrgAb/hEQRlsTW3rRkVh+rrdoUE7BCZRTFmRCbLoShjN6XuSf4sAus8ch4UEN12gN0CgYEA4o/tSvij1iPaXLmt4KOEuxqmSGB8MLKhFde8lBbNdrDgxiIH9bH7khKx15XRTX0qLDbs8b2/UJygZG0Aa1kIBqZTXTgeMAuxPRTesALJPdqQ/ROnbJcdFkI7gllrAG8VB0fH4wTRsRd0vWEB6YlCdE107u6LEsLAHxOj9Q5819cCgYEAwXjx9RkQ2qITBx5Ewib8YsltA0n3cmRomPicLlsnKV5DfvyCLpFIsZ1h3f9dUpfxRLwzp8wcoLiq9cCoOGdu1udw/yBTqmhaXWhUK/g77f9Ze2ZB1OEhuyKLYJ1vW/h/Z/a1aPCMxZqsDTPCePsuO8Cez5gqs8LjM3W7EyzRxDMCgYEAvhHrDFt975fSiLoJgo0MPIAGAnBXn+8sLwv3m/FpW+rWF8LTFK/Fku12H5wDpNOdvswxijkauIE+GiJMGMLvdcyx4WHECaC1h73reJRNykOEIZ0Md5BrCZJ1JEzp9Mo8RQhWTEFtvfkkqgApP4g0pSeaMx0StaGG1kt+4IbP+68CgYBrZdQKlquAck/Vt7u7eyDHRcE5/ilaWtqlb/xizz7h++3D5C/v4b5UumTFcyg+3RGVclPKZcfOgDSGzzeSd/hTW46iUTOgeOUQzQVMkzPRXdoyYgVRQtgSpY5xR3O1vjAbahwx8LZ0SvQPMBhYSDbV/Isr+fBacWjl/AipEEwxeQKBgQDdrAEnVlOFoCLw4sUjsPoxkLjhTAgI7CYk5NNxX67Rnj0tp+Y49+sGUhl5sCGfMKkLShiON5P2oxZa+B0aPtQjsdnsFPa1uaZkK4c++SS6AetzYRpVDLmLp7/1CulE0z3O0sBekpwiuaqLJ9ZccC81g4+2j8j6c50rIAct3hxIxw==", //nolint:lll
|
||||
TLSAuth: "48d9999bd71095b10649c7cb471c1051b1afdece597cea06909b99303a18c67401597b12c04a787e98cdb619ee960d90a0165529dc650f3a5c6fbe77c91c137dcf55d863fcbe314df5f0b45dbe974d9bde33ef5b4803c3985531c6c23ca6906d6cd028efc8585d1b9e71003566bd7891b9cc9212bcba510109922eed87f5c8e66d8e59cbd82575261f02777372b2cd4ca5214c4a6513ff26dd568f574fd40d6cd450fc788160ff68434ce2bf6afb00e710a3198538f14c4d45d84ab42637872e778a6b35a124e700920879f1d003ba93dccdb953cdf32bea03f365760b0ed8002098d4ce20d045b45a83a8432cc737677aed27125592a7148d25c87fdbe0a3f6", //nolint:lll
|
||||
Auth: openvpn.SHA512,
|
||||
CAs: []string{
|
||||
"MIIGqjCCBJKgAwIBAgIUfTu1OKHHguAcfIyUn3CIZl2EMDcwDQYJKoZIhvcNAQENBQAwgYUxCzAJBgNVBAYTAlZHMQwwCgYDVQQIDANCVkkxEzARBgNVBAoMCkV4cHJlc3NWUE4xEzARBgNVBAsMCkV4cHJlc3NWUE4xFzAVBgNVBAMMDkV4cHJlc3NWUE4gQ0EzMSUwIwYJKoZIhvcNAQkBFhZzdXBwb3J0QGV4cHJlc3N2cG4uY29tMCAXDTI0MTEwNjA0MzE1M1oYDzIxMjQxMDEzMDQzMTUzWjCBhTELMAkGA1UEBhMCVkcxDDAKBgNVBAgMA0JWSTETMBEGA1UECgwKRXhwcmVzc1ZQTjETMBEGA1UECwwKRXhwcmVzc1ZQTjEXMBUGA1UEAwwORXhwcmVzc1ZQTiBDQTMxJTAjBgkqhkiG9w0BCQEWFnN1cHBvcnRAZXhwcmVzc3Zwbi5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCWIv5F4B+LjenICyenASeml80jllmV71080/XPSA9NaygXLr5ui9NPyjKrn7vL74HnmCEgPEU0yysWCY29pnF7yid182pl8CMM+naAcIDFJd6jR4YfWmJZ4Djj9w3WK/pIWw/gXl3UPyqiN7TziainkH4RFM/S0/08IOjYvqD7HhcxZFj5cfWo/wW7lHNmlnDkQx/FuYEqLCfBKoLer2kVPHu0b/QdLZ4cp/dLAuFjbQdaxXsywMxLldRs8ToMaFuoWdrJkohlmBlXqt1IGKUUht4Ju2Nqdgi8CsMd63XAWit+Gr+d+0AI4nkft5PpNjfulbGlyZLqXSd4D96s3nQqVzjZczTAYNxT6yVZ8K0IDbRbEFGvBZ5n/5jNQaqTTm7yNcrmqbfL8EFeDWAZmY33SSgTP4fsA0HC3G3bcuxBk0pcBqCvFYxDPzsfVXlb1Uw3lZyY1Km4AsDQqZQdl5ZRFIEklZdsNELVNveyusPlLAQunwRIEFnYzZTCwhMc9sOY8DsaC1Zcn1dlPenetxMacHC4vOtqgekMubH9pFrqutA2c3Ck1fRxDUXw6AbRrZRX/BrHegfE1GkKKXwUuazSi+3FbBniu4a7bV2RFLYo8Gmo01DzMK5/0rGilpW8mU1q6YwHYSKlxutwN2BWJtXc4dzqE5A5TnfoZgp0gZHOhwIDAQABo4IBDDCCAQgwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUM9vH/Agamn13MFeU9ctFB5culQIwgcUGA1UdIwSBvTCBuoAUM9vH/Agamn13MFeU9ctFB5culQKhgYukgYgwgYUxCzAJBgNVBAYTAlZHMQwwCgYDVQQIDANCVkkxEzARBgNVBAoMCkV4cHJlc3NWUE4xEzARBgNVBAsMCkV4cHJlc3NWUE4xFzAVBgNVBAMMDkV4cHJlc3NWUE4gQ0EzMSUwIwYJKoZIhvcNAQkBFhZzdXBwb3J0QGV4cHJlc3N2cG4uY29tghR9O7U4oceC4Bx8jJSfcIhmXYQwNzAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQENBQADggIBABZtroQt7d8yy8CN60ErYPbLcwf93iZxDyvqSOqV6si7A4sF0KGDnS6zznsn9aJ+ZNYRYAI0WtabIkq1mtmdw1fMnC34ywl/28AcumdBM8gv48bE58pwySOeYZNPC+4yTCHIzc322ojP2YhLRKUM0IH9+N3IxmoCFIdEKbGiXEsW4zZahWRBgxr2Ew3D6N8RKsdMrSPw7lvW9eSs3s88lYXF+FtGp5Wid9bzmCa3tgySA7gmNAkLNbm2O8NdM8gBIlCDOI3u8FC7SDS7QyoMn8oeRxlkBkby5OKsZ5j10hSDHEdGrHqNn1bAGfpuRfZVg9kPvnTomjCo2TcD1Ig6iOt6IAKAaOZNgYYT/5ttA8q4Uum8lTYdtQRTWDWHBKYcMjvhWwvhjumYnlN6eaGhsHZEsFBpgHwV454zTMRX6oRbdaJwBGYhODoI3hxB14zqiK/BJi9mq2OQOrfh2MBBrV1w63YkJ0rxXs1PEhx1iI7zjLtGMgBzG2Y7sAa/z3Uo6uAaA7jj+eig3bmZ5Iatw1pfqEQT/M1A/H5aUYq4KOPBB8AkRzpHty003CJrYcr+LsdotRTiqYxB9QAqs7u5WZ82XiYOImN3SgrTcJQPHXWtbUmsx6pxCkHelMMgWCfPSkWGBQCYm/vuOx6Ysea22jH0zuy8GCTYASy7w6ks9JBe", //nolint:lll
|
||||
"MIIF+DCCA+CgAwIBAgIBATANBgkqhkiG9w0BAQ0FADCBhDELMAkGA1UEBhMCVkcxDDAKBgNVBAgMA0JWSTETMBEGA1UECgwKRXhwcmVzc1ZQTjETMBEGA1UECwwKRXhwcmVzc1ZQTjEWMBQGA1UEAwwNRXhwcmVzc1ZQTiBDQTElMCMGCSqGSIb3DQEJARYWc3VwcG9ydEBleHByZXNzdnBuLmNvbTAeFw0xNTEwMjEwMDAwMDBaFw0yNjA0MDEyMTEyMDBaMIGEMQswCQYDVQQGEwJWRzEMMAoGA1UECAwDQlZJMRMwEQYDVQQKDApFeHByZXNzVlBOMRMwEQYDVQQLDApFeHByZXNzVlBOMRYwFAYDVQQDDA1FeHByZXNzVlBOIENBMSUwIwYJKoZIhvcNAQkBFhZzdXBwb3J0QGV4cHJlc3N2cG4uY29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxzXvHZ25OsESKRMQFINHJNqE9kVRLWJS50oVB2jxobudPhCsWvJSApvar8CB2RrqkVMhXu2HT3FBtDL91INg070qAyjjRpzEbDPWqQ1+G0tk0sjiJt2mXPJK2IlNFnhe6rTs09Pkpcp8qRhfZay/dIlmagohQAr4JvYL1Ajg9A3sLb8JkY03H6GhOF8EKYTqhrEppCcg4sQKQhNSytRoQAm8Ta+tnTYIedwWpqjUXP9YXFOvljPaixfYug24eAkpTjeuWTcELSyfnuiBeK+z9+5OYunhqFt2QZMq33kLFZGMN2gHRCzngxxphurypsPRo7jiFgQI1yLt8uZsEZ+otGEK91jjKfOC+g9TBy2RUtxk1neWcQ6syXDuc3rBNrGA8iM0ZoEqQ1BC8xWr3NYlSjqN+1mgpTAX3/Dxze4GzHd7AmYaYJV8xnKBVNphlMlg1giCAu5QXjMxPbfCgZiEFq/uq0SOKQJeT3AI/uVPSvwCMWByjyMbDpKKAK8Hy3UT5m4bCNu8J7bxj+vdnq0A2HPwtF0FwBl/TIM3zNsyFrZZ0j6jLRT50mFsgDBKcD4L/J5rjdCsKPu5rodhxe38rCx2GknP1Zkov4yoVCcR48+CQwg3oBkq0/EflvWUvcYApzs9SomUM/g+8Q/V0WOfJmFWuxN9YntZlnzHRSRjrvMCAwEAAaNzMHEwHQYDVR0OBBYEFIzmQGj8xS+0LLklwqHD45VVOZRJMB8GA1UdIwQYMBaAFIzmQGj8xS+0LLklwqHD45VVOZRJMA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIBFjANBgkqhkiG9w0BAQ0FAAOCAgEAbHfuMKtojm1NgX7qSU2Rm2B5L8G0FuFP0L40dj8O5WHt45j2z8coMK90vrUnQEZNQmRzot7v3XjVzVlxBWYSsCEApTsSDNi/4BNFP8H/BUUtJuy2GFTO4wDVJnqNkZOHBmyVD75s1Y+W8a+zB4jkMeDEhOHZdwQ0l1fJDDgXal5f1UT5F5WH6/RwHmWTwX4GxuCiIVtx70CjkXqhM8yZtTp1UtHLRNYcNSIes0vrAPHPgoA5z9B8UvsOjuP+mfcjzi0LGGrY+2pJu0BKO2dRnarIZZABETIisI3FokoTszx5jpRPyxyUTuRDKWHrvi0PPtOmC8nFahfugWFUi6uBsqCaSeuex+ahnTPCq0b1l0Ozpg0YeE8CW1TL9Y92b01up2c+PP6wZOIm3JyTH+L5smDFbh80V42dKyGNdPXMg5IcJhj3YfAy4k8h/qbWY57KFcIzKx40bFsoI7PeydbGtT/dIoFLSZRLW5bleXNgG9mXZp270UeEC6CpATCS6uVl8LVT1I02uulHUpFaRmTEOrmMxsXGt6UAwYTY55K/B8uuID341xKbeC0kzhuN2gsL5UJaocBHyWK/AqwbeBttdhOCLwoaj7+nSViPxICObKrg3qavGNCvtwy/fEegK9X/wlp2e2CFlIhFbadeXOBr9Fn8ypYPP17mTqe98OJYM04=", //nolint:lll
|
||||
},
|
||||
Cert: "MIIDTjCCAregAwIBAgIDKzZvMA0GCSqGSIb3DQEBCwUAMIGFMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFTATBgNVBAcTDFNhbkZyYW5jaXNjbzEVMBMGA1UEChMMRm9ydC1GdW5zdG9uMRgwFgYDVQQDEw9Gb3J0LUZ1bnN0b24gQ0ExITAfBgkqhkiG9w0BCQEWEm1lQG15aG9zdC5teWRvbWFpbjAgFw0xNjExMDMwMzA2MThaGA8yMDY2MTEwMzAzMDYxOFowgYoxCzAJBgNVBAYTAlZHMQwwCgYDVQQIDANCVkkxEzARBgNVBAoMCkV4cHJlc3NWUE4xEzARBgNVBAsMCkV4cHJlc3NWUE4xHDAaBgNVBAMME2V4cHJlc3N2cG5fY3VzdG9tZXIxJTAjBgkqhkiG9w0BCQEWFnN1cHBvcnRAZXhwcmVzc3Zwbi5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrOYt/KOi2uMDGev3pXg8j1SO4J/4EVWDF7vJcKr2jrZlqD/zuAFx2W1YWvwumPO6PKH4PU9621aNdiumaUkv/RplCfznnnxqobhJuTE2oA+rS1bOq+9OhHwF9jgNXNVk+XX4d0toST5uGE6Z3OdmPBur8o5AlCf78PDSAwpFOw5HrgLqOEU4hTweC1/czX2VsvsHv22HRI6JMZgP8gGQii/p9iukqfaJvGdPciL5p1QRBUQIi8P8pNvEp1pVIpxYj7/LOUqb2DxFvgmp2v1IQ0Yu88SWsFk84+xAYHzfkLyS31Sqj5uLRBnJqx3fIlOihQ50GI72fwPMwo+OippvVAgMBAAGjPzA9MAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgSwMB0GA1UdDgQWBBSkBM1TCX9kBgFsv2RmOzudMXa9njANBgkqhkiG9w0BAQsFAAOBgQA+2e4b+33zFmA+1ZQ46kWkfiB+fEeDyMwMLeYYyDS2d8mZhNZKdOw7dy4Ifz9Vqzp4aKuQ6j61c6k1UaQQL0tskqWVzslSFvs9NZyUAJLLdGUc5TT2MiLwiXQwd4UvH6bGeePdhvB4+ZbW7VMD7TE8hZhjhAL4F6yAP1EQvg3LDA==", //nolint:lll
|
||||
RSAKey: "MIIEpAIBAAKCAQEAqzmLfyjotrjAxnr96V4PI9UjuCf+BFVgxe7yXCq9o62Zag/87gBcdltWFr8Lpjzujyh+D1PettWjXYrpmlJL/0aZQn85558aqG4SbkxNqAPq0tWzqvvToR8BfY4DVzVZPl1+HdLaEk+bhhOmdznZjwbq/KOQJQn+/Dw0gMKRTsOR64C6jhFOIU8Hgtf3M19lbL7B79th0SOiTGYD/IBkIov6fYrpKn2ibxnT3Ii+adUEQVECIvD/KTbxKdaVSKcWI+/yzlKm9g8Rb4Jqdr9SENGLvPElrBZPOPsQGB835C8kt9Uqo+bi0QZyasd3yJTooUOdBiO9n8DzMKPjoqab1QIDAQABAoIBAHgsekC0SKi+AOcNOZqJ3pxqophE0V7fQX2KWGXhxZnUZMFxGTc936deMYzjZ1y0lUa6x8cgOUcfqHol3hDmw9oWBckLHGv5Wi9umdb6DOLoZO62+FQATSdfaJ9jheq2Ub2YxsRN0apaXzB6KDKz0oM0+sZ4Udn9Kw6DfuIELRIWwEx4w0v3gKW7YLC4Jkc4AwLkPK03xEA/qImfkCmaMPLhrgVQt+IFfP8bXzL7CCC04rNU/IS8pyjex+iUolnQZlbXntF7Bm4V2mz0827ZVqrgAb/hEQRlsTW3rRkVh+rrdoUE7BCZRTFmRCbLoShjN6XuSf4sAus8ch4UEN12gN0CgYEA4o/tSvij1iPaXLmt4KOEuxqmSGB8MLKhFde8lBbNdrDgxiIH9bH7khKx15XRTX0qLDbs8b2/UJygZG0Aa1kIBqZTXTgeMAuxPRTesALJPdqQ/ROnbJcdFkI7gllrAG8VB0fH4wTRsRd0vWEB6YlCdE107u6LEsLAHxOj9Q5819cCgYEAwXjx9RkQ2qITBx5Ewib8YsltA0n3cmRomPicLlsnKV5DfvyCLpFIsZ1h3f9dUpfxRLwzp8wcoLiq9cCoOGdu1udw/yBTqmhaXWhUK/g77f9Ze2ZB1OEhuyKLYJ1vW/h/Z/a1aPCMxZqsDTPCePsuO8Cez5gqs8LjM3W7EyzRxDMCgYEAvhHrDFt975fSiLoJgo0MPIAGAnBXn+8sLwv3m/FpW+rWF8LTFK/Fku12H5wDpNOdvswxijkauIE+GiJMGMLvdcyx4WHECaC1h73reJRNykOEIZ0Md5BrCZJ1JEzp9Mo8RQhWTEFtvfkkqgApP4g0pSeaMx0StaGG1kt+4IbP+68CgYBrZdQKlquAck/Vt7u7eyDHRcE5/ilaWtqlb/xizz7h++3D5C/v4b5UumTFcyg+3RGVclPKZcfOgDSGzzeSd/hTW46iUTOgeOUQzQVMkzPRXdoyYgVRQtgSpY5xR3O1vjAbahwx8LZ0SvQPMBhYSDbV/Isr+fBacWjl/AipEEwxeQKBgQDdrAEnVlOFoCLw4sUjsPoxkLjhTAgI7CYk5NNxX67Rnj0tp+Y49+sGUhl5sCGfMKkLShiON5P2oxZa+B0aPtQjsdnsFPa1uaZkK4c++SS6AetzYRpVDLmLp7/1CulE0z3O0sBekpwiuaqLJ9ZccC81g4+2j8j6c50rIAct3hxIxw==", //nolint:lll
|
||||
TLSAuth: "48d9999bd71095b10649c7cb471c1051b1afdece597cea06909b99303a18c67401597b12c04a787e98cdb619ee960d90a0165529dc650f3a5c6fbe77c91c137dcf55d863fcbe314df5f0b45dbe974d9bde33ef5b4803c3985531c6c23ca6906d6cd028efc8585d1b9e71003566bd7891b9cc9212bcba510109922eed87f5c8e66d8e59cbd82575261f02777372b2cd4ca5214c4a6513ff26dd568f574fd40d6cd450fc788160ff68434ce2bf6afb00e710a3198538f14c4d45d84ab42637872e778a6b35a124e700920879f1d003ba93dccdb953cdf32bea03f365760b0ed8002098d4ce20d045b45a83a8432cc737677aed27125592a7148d25c87fdbe0a3f6", //nolint:lll
|
||||
MssFix: 1200,
|
||||
FastIO: true,
|
||||
Fragment: 1300,
|
||||
|
||||
@@ -108,7 +108,6 @@ func hardcodedServers() (servers []models.Server) {
|
||||
{Country: "New Zealand", Hostname: "newzealand-ca-version-2.expressnetw.com"},
|
||||
{Country: "North Macedonia", Hostname: "macedonia-ca-version-2.expressnetw.com"},
|
||||
{Country: "Norway", Hostname: "norway-ca-version-2.expressnetw.com"},
|
||||
{Country: "Pakistan", Hostname: "pakistan-ca-version-2.expressnetw.com"},
|
||||
{Country: "Panama", Hostname: "panama-ca-version-2.expressnetw.com"},
|
||||
{Country: "Peru", Hostname: "peru-ca-version-2.expressnetw.com"},
|
||||
{Country: "Philippines (via Singapore)", Hostname: "ph-via-sing-ca-version-2.expressnetw.com"},
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) (
|
||||
connection models.Connection, err error,
|
||||
) {
|
||||
defaults := utils.NewConnectionDefaults(0, 1194, 0) //nolint:mnd
|
||||
defaults := utils.NewConnectionDefaults(1194, 1194, 0) //nolint:mnd
|
||||
return utils.GetConnection(p.Name(),
|
||||
p.storage, selection, defaults, ipv6Supported, p.randSource)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package privado
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"net/http"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||
"github.com/qdm12/gluetun/internal/provider/common"
|
||||
@@ -15,14 +16,12 @@ type Provider struct {
|
||||
}
|
||||
|
||||
func New(storage common.Storage, randSource rand.Source,
|
||||
ipFetcher common.IPFetcher, unzipper common.Unzipper,
|
||||
updaterWarner common.Warner,
|
||||
parallelResolver common.ParallelResolver,
|
||||
client *http.Client, updaterWarner common.Warner,
|
||||
) *Provider {
|
||||
return &Provider{
|
||||
storage: storage,
|
||||
randSource: randSource,
|
||||
Fetcher: updater.New(ipFetcher, unzipper, updaterWarner, parallelResolver),
|
||||
Fetcher: updater.New(client, updaterWarner),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
type hostToServer map[string]models.Server
|
||||
|
||||
func (hts hostToServer) add(host string) {
|
||||
server, ok := hts[host]
|
||||
if ok {
|
||||
return
|
||||
}
|
||||
server.VPN = vpn.OpenVPN
|
||||
server.Hostname = host
|
||||
server.UDP = true
|
||||
hts[host] = server
|
||||
}
|
||||
|
||||
func (hts hostToServer) toHostsSlice() (hosts []string) {
|
||||
hosts = make([]string, 0, len(hts))
|
||||
for host := range hts {
|
||||
hosts = append(hosts, host)
|
||||
}
|
||||
return hosts
|
||||
}
|
||||
|
||||
func (hts hostToServer) adaptWithIPs(hostToIPs map[string][]netip.Addr) {
|
||||
for host, IPs := range hostToIPs {
|
||||
server := hts[host]
|
||||
server.IPs = IPs
|
||||
hts[host] = server
|
||||
}
|
||||
for host, server := range hts {
|
||||
if len(server.IPs) == 0 {
|
||||
delete(hts, host)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (hts hostToServer) toServersSlice() (servers []models.Server) {
|
||||
servers = make([]models.Server, 0, len(hts))
|
||||
for _, server := range hts {
|
||||
servers = append(servers, server)
|
||||
}
|
||||
return servers
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/provider/common"
|
||||
"github.com/qdm12/gluetun/internal/publicip/api"
|
||||
)
|
||||
|
||||
func setLocationInfo(ctx context.Context, fetcher common.IPFetcher, servers []models.Server) (err error) {
|
||||
// Get public IP address information
|
||||
ipsToGetInfo := make([]netip.Addr, 0, len(servers))
|
||||
for _, server := range servers {
|
||||
ipsToGetInfo = append(ipsToGetInfo, server.IPs...)
|
||||
}
|
||||
ipsInfo, err := api.FetchMultiInfo(ctx, fetcher, ipsToGetInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range servers {
|
||||
ipInfo := ipsInfo[i]
|
||||
servers[i].Country = ipInfo.Country
|
||||
servers[i].Region = ipInfo.Region
|
||||
servers[i].City = ipInfo.City
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/updater/resolver"
|
||||
)
|
||||
|
||||
func parallelResolverSettings(hosts []string) (settings resolver.ParallelSettings) {
|
||||
const (
|
||||
maxFailRatio = 0.1
|
||||
maxDuration = 30 * time.Second
|
||||
maxNoNew = 2
|
||||
maxFails = 2
|
||||
)
|
||||
return resolver.ParallelSettings{
|
||||
Hosts: hosts,
|
||||
MaxFailRatio: maxFailRatio,
|
||||
Repeat: resolver.RepeatSettings{
|
||||
MaxDuration: maxDuration,
|
||||
MaxNoNew: maxNoNew,
|
||||
MaxFails: maxFails,
|
||||
SortIPs: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -2,77 +2,66 @@ package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants/vpn"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/provider/common"
|
||||
"github.com/qdm12/gluetun/internal/updater/openvpn"
|
||||
)
|
||||
|
||||
func (u *Updater) FetchServers(ctx context.Context, minServers int) (
|
||||
servers []models.Server, err error,
|
||||
) {
|
||||
if !u.ipFetcher.CanFetchAnyIP() {
|
||||
return nil, fmt.Errorf("%w: %s", common.ErrIPFetcherUnsupported, u.ipFetcher.String())
|
||||
}
|
||||
|
||||
const url = "https://privadovpn.com/apps/ovpn_configs.zip"
|
||||
contents, err := u.unzipper.FetchAndExtract(ctx, url)
|
||||
const url = "https://privadovpn.com/apps/servers_export.json"
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if len(contents) < minServers {
|
||||
return nil, fmt.Errorf("%w: %d and expected at least %d",
|
||||
common.ErrNotEnoughServers, len(contents), minServers)
|
||||
return nil, fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
|
||||
hts := make(hostToServer)
|
||||
|
||||
for fileName, content := range contents {
|
||||
if !strings.HasSuffix(fileName, ".ovpn") {
|
||||
continue // not an OpenVPN file
|
||||
}
|
||||
|
||||
host, warning, err := openvpn.ExtractHost(content)
|
||||
if warning != "" {
|
||||
u.warner.Warn(warning)
|
||||
}
|
||||
if err != nil {
|
||||
// treat error as warning and go to next file
|
||||
u.warner.Warn(err.Error() + " in " + fileName)
|
||||
continue
|
||||
}
|
||||
|
||||
hts.add(host)
|
||||
}
|
||||
|
||||
if len(hts) < minServers {
|
||||
return nil, fmt.Errorf("%w: %d and expected at least %d",
|
||||
common.ErrNotEnoughServers, len(hts), minServers)
|
||||
}
|
||||
|
||||
hosts := hts.toHostsSlice()
|
||||
resolveSettings := parallelResolverSettings(hosts)
|
||||
hostToIPs, warnings, err := u.parallelResolver.Resolve(ctx, resolveSettings)
|
||||
for _, warning := range warnings {
|
||||
u.warner.Warn(warning)
|
||||
}
|
||||
response, err := u.client.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(hostToIPs) < minServers {
|
||||
return nil, fmt.Errorf("%w: %d and expected at least %d",
|
||||
common.ErrNotEnoughServers, len(servers), minServers)
|
||||
var data struct {
|
||||
Servers []struct {
|
||||
Country string `json:"country"`
|
||||
City string `json:"city"`
|
||||
Hostname string `json:"hostname"`
|
||||
IP netip.Addr `json:"ip"`
|
||||
} `json:"servers"`
|
||||
}
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
err = decoder.Decode(&data)
|
||||
if err != nil {
|
||||
_ = response.Body.Close()
|
||||
return nil, fmt.Errorf("decoding JSON response: %w", err)
|
||||
}
|
||||
|
||||
hts.adaptWithIPs(hostToIPs)
|
||||
err = response.Body.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("closing response body: %w", err)
|
||||
}
|
||||
|
||||
servers = hts.toServersSlice()
|
||||
if len(data.Servers) < minServers {
|
||||
return nil, fmt.Errorf("%w: %d and expected at least %d",
|
||||
common.ErrNotEnoughServers, len(data.Servers), minServers)
|
||||
}
|
||||
|
||||
if err := setLocationInfo(ctx, u.ipFetcher, servers); err != nil {
|
||||
return nil, err
|
||||
servers = make([]models.Server, len(data.Servers))
|
||||
for i, server := range data.Servers {
|
||||
servers[i] = models.Server{
|
||||
VPN: vpn.OpenVPN,
|
||||
Country: server.Country,
|
||||
City: server.City,
|
||||
Hostname: server.Hostname,
|
||||
IPs: []netip.Addr{server.IP},
|
||||
UDP: true,
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(models.SortableServers(servers))
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/provider/common"
|
||||
)
|
||||
|
||||
type Updater struct {
|
||||
ipFetcher common.IPFetcher
|
||||
unzipper common.Unzipper
|
||||
parallelResolver common.ParallelResolver
|
||||
warner common.Warner
|
||||
client *http.Client
|
||||
warner common.Warner
|
||||
}
|
||||
|
||||
func New(ipFetcher common.IPFetcher, unzipper common.Unzipper,
|
||||
warner common.Warner, parallelResolver common.ParallelResolver,
|
||||
) *Updater {
|
||||
func New(client *http.Client, warner common.Warner) *Updater {
|
||||
return &Updater{
|
||||
ipFetcher: ipFetcher,
|
||||
unzipper: unzipper,
|
||||
parallelResolver: parallelResolver,
|
||||
warner: warner,
|
||||
client: client,
|
||||
warner: warner,
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user