feat(others): optional BORINGPOLL_GLUETUNCOM to fight AI slop scammy gluetun[dot]com

This commit is contained in:
Quentin McGaw
2026-03-06 16:27:16 +00:00
parent 2460b56c2b
commit 457e5597bb
12 changed files with 287 additions and 8 deletions
+3
View File
@@ -18,5 +18,8 @@ jobs:
If you think this is *still unresolved* and have **more information** to bring, please either
re-open this issue or create another issue.
❤️😠 temporarily help the Gluetun community and fight the AI slop scam website `gluetun[dot]com` by setting `BORINGPOLL_GLUETUNCOM=on` on the latest image.
See [the option in the wiki for more information](https://github.com/qdm12/gluetun-wiki/blob/main/setup/options/others.md)
This is an automated comment setup because @qdm12 is the sole maintainer of this project
which became too popular to monitor closed issues.
+1
View File
@@ -231,6 +231,7 @@ ENV VPN_SERVICE_PROVIDER=pia \
PPROF_HTTP_SERVER_ADDRESS=":6060" \
# Extras
VERSION_INFORMATION=on \
BORINGPOLL_GLUETUNCOM=off \
TZ= \
PUID=1000 \
PGID=1000
+2
View File
@@ -2,6 +2,8 @@
⚠️ This and [gluetun-wiki](https://github.com/qdm12/gluetun-wiki) are the only websites for Gluetun, other websites claiming to be official are scams ⚠️
💁 You can optionally set `BORINGPOLL_GLUETUNCOM=on` to... [poll](./internal/boringpoll/boringpoll.go) that **scammy AI slop** website every few minutes so it costs them too much to keep it up. My gentle email reminders to take it down are being grossly ignored 🤷 This would make me very happy and serve this community.
Lightweight swiss-army-knife-like VPN client to multiple VPN service providers
![Title image](https://raw.githubusercontent.com/qdm12/gluetun/master/title.svg)
+9 -5
View File
@@ -17,6 +17,7 @@ import (
_ "github.com/breml/rootcerts"
"github.com/qdm12/gluetun/internal/alpine"
"github.com/qdm12/gluetun/internal/boringpoll"
"github.com/qdm12/gluetun/internal/cli"
"github.com/qdm12/gluetun/internal/command"
"github.com/qdm12/gluetun/internal/configuration/settings"
@@ -168,7 +169,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
defer fmt.Println(gluetunLogo)
announcementExp, err := time.Parse(time.RFC3339, "2026-04-01T00:00:00Z")
announcementExp, err := time.Parse(time.RFC3339, "2026-04-30T00:00:00Z")
if err != nil {
return err
}
@@ -179,7 +180,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
Version: buildInfo.Version,
Commit: buildInfo.Commit,
Created: buildInfo.Created,
Announcement: "All control server routes are now private by default",
Announcement: "Set BORINGPOLL_GLUETUNCOM=on to help combat AI slop and shutdown that scam website",
AnnounceExp: announcementExp,
// Sponsor information
PaypalUser: "qmcgaw",
@@ -441,11 +442,14 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(),
openvpnFileExtractor, allSettings.Updater)
boringPollLogger := logger.New(log.SetComponent("boring poll"))
boringPoll := boringpoll.New(httpClient, boringPollLogger, allSettings.BoringPoll)
vpnLogger := logger.New(log.SetComponent("vpn"))
vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6Supported, allSettings.Firewall.VPNInputPorts,
providers, storage, allSettings.Health, healthChecker, healthcheckServer, ovpnConf, netLinker, firewallConf,
routingConf, portForwardLooper, cmder, publicIPLooper, dnsLooper, vpnLogger, httpClient,
buildInfo, *allSettings.Version.Enabled)
providers, storage, boringPoll, allSettings.Health, healthChecker, healthcheckServer,
ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper, cmder, publicIPLooper,
dnsLooper, vpnLogger, httpClient, buildInfo, *allSettings.Version.Enabled)
vpnHandler, vpnCtx, vpnDone := goshutdown.NewGoRoutineHandler(
"vpn", goroutine.OptionTimeout(time.Second))
go vpnLooper.Run(vpnCtx, vpnDone)
+188
View File
@@ -0,0 +1,188 @@
package boringpoll
import (
"context"
"fmt"
"io"
"math/rand"
"net/http"
"sync"
"time"
"github.com/qdm12/gluetun/internal/configuration/settings"
)
type BoringPoll struct {
// Injected dependencies
client *http.Client
logger Logger
// Internal state
urlToData map[string]*urlData
// Internal signals and channels
cancel context.CancelFunc
done <-chan struct{}
mutex sync.Mutex
}
type urlData struct{}
func New(client *http.Client, logger Logger, settings settings.BoringPoll) *BoringPoll {
urlToData := make(map[string]*urlData)
if *settings.GluetunCom {
urlToData["https://gluetun.com/wp-json"] = &urlData{}
}
return &BoringPoll{
client: client,
logger: logger,
urlToData: urlToData,
}
}
func (b *BoringPoll) Start() (runError <-chan error, err error) {
b.mutex.Lock()
defer b.mutex.Unlock()
if len(b.urlToData) == 0 {
return nil, nil //nolint:nilnil
}
const minPeriod = time.Minute
const maxPeriod = 5 * time.Minute
const logEveryBytes = 100 * 1000 * 1000 // 100 IEC MB
var ready, done sync.WaitGroup
ready.Add(len(b.urlToData))
done.Add(len(b.urlToData))
ctx, cancel := context.WithCancel(context.Background())
b.cancel = cancel
for url := range b.urlToData {
go func(url string) {
defer done.Done()
b.logger.Infof("running against %s periodically between %s and %s "+
"and will log every %s downloaded",
url, minPeriod, maxPeriod, byteCountSI(logEveryBytes))
totalDownloaded := uint64(0)
lastDownloaded := uint64(0)
consecutiveFails := 0
const maxConsecutiveErrs = 3
const coolDownTimeout = time.Hour
timer := time.NewTimer(time.Hour)
var err error
ready.Done()
for {
timeout := minPeriod + time.Duration(rand.Int63n(int64(maxPeriod-minPeriod))) //nolint:gosec
if consecutiveFails >= maxConsecutiveErrs {
b.logger.Debugf("pausing poll to %s for %s due to %d consecutive errors, last error: %s",
url, coolDownTimeout, consecutiveFails, err)
timeout = coolDownTimeout
}
timer.Reset(timeout)
select {
case <-ctx.Done():
timer.Stop()
totalDownloaded += lastDownloaded
if totalDownloaded > 0 {
b.logger.Infof("stopping poll to %s, downloaded %s!", url, byteCountSI(totalDownloaded))
}
return
case <-timer.C:
}
n, err := fetchURL(ctx, b.client, url)
if err != nil {
consecutiveFails++
continue
}
consecutiveFails = 0
totalDownloaded += uint64(n) //nolint:gosec
lastDownloaded += uint64(n) //nolint:gosec
if lastDownloaded >= logEveryBytes {
b.logger.Infof("thanks for helping! Downloaded %s from %s!",
byteCountSI(totalDownloaded), url)
lastDownloaded = 0
}
}
}(url)
}
return nil, nil //nolint:nilnil
}
func fetchURL(ctx context.Context, client *http.Client, url string) (downloaded int64, err error) {
const requestTimeout = 10 * time.Second
ctx, cancel := context.WithTimeout(ctx, requestTimeout)
defer cancel()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
cancel()
return 0, err
}
request.Header.Set("Cache-Control", "no-cache, no-store, must-revalidate")
request.Header.Set("Pragma", "no-cache")
request.Header.Set("Expires", "0")
request.Header.Set("User-Agent", getRandomUserAgent())
response, err := client.Do(request)
if err != nil {
return 0, err
}
downloaded, err = io.Copy(io.Discard, response.Body)
_ = response.Body.Close()
if err != nil {
return 0, err
}
return downloaded, nil
}
func getRandomUserAgent() string {
//nolint:lll
userAgents := [...]string{
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/121.0.0.0 Safari/537.36",
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_2_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1",
"Mozilla/5.0 (iPad; CPU OS 17_2_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1",
"Mozilla/5.0 (Android 14; Mobile; rv:122.0) Gecko/122.0 Firefox/122.0",
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 OPR/106.0.0.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 OPR/106.0.0.0",
"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)",
"Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)",
}
return userAgents[rand.Intn(len(userAgents))] //nolint:gosec
}
func (b *BoringPoll) Stop() error {
b.mutex.Lock()
defer b.mutex.Unlock()
if b.cancel == nil {
return nil
}
b.cancel()
<-b.done
b.cancel = nil
b.done = nil
return nil
}
func byteCountSI(b uint64) string {
const unit = 1000
if b < unit {
return fmt.Sprintf("%dB", b)
}
div, exp := uint64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f%cB", float64(b)/float64(div), "kMGTPE"[exp])
}
+6
View File
@@ -0,0 +1,6 @@
package boringpoll
type Logger interface {
Infof(format string, args ...any)
Debugf(format string, args ...any)
}
@@ -0,0 +1,51 @@
package settings
import (
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
)
type BoringPoll struct {
GluetunCom *bool
}
func (b BoringPoll) validate() error {
return nil
}
func (b BoringPoll) Copy() BoringPoll {
return BoringPoll{
GluetunCom: gosettings.CopyPointer(b.GluetunCom),
}
}
func (b *BoringPoll) overrideWith(other BoringPoll) {
b.GluetunCom = gosettings.OverrideWithPointer(b.GluetunCom, other.GluetunCom)
}
func (b *BoringPoll) setDefaults() {
b.GluetunCom = gosettings.DefaultPointer(b.GluetunCom, false)
}
func (b BoringPoll) String() string {
return b.toLinesNode().String()
}
func (b BoringPoll) toLinesNode() *gotree.Node {
if !*b.GluetunCom {
return nil
}
node := gotree.New("Boring-poll settings:")
node.Append("gluetun.com: on")
return node
}
func (b *BoringPoll) read(r *reader.Reader) (err error) {
b.GluetunCom, err = r.BoolPtr("BORINGPOLL_GLUETUNCOM")
if err != nil {
return err
}
return nil
}
@@ -28,6 +28,7 @@ type Settings struct {
Version Version
VPN VPN
Pprof pprof.Settings
BoringPoll BoringPoll
}
type FilterChoicesGetter interface {
@@ -57,6 +58,7 @@ func (s *Settings) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Support
"VPN": func() error {
return s.VPN.Validate(filterChoicesGetter, ipv6Supported, warner)
},
"boring poll": s.BoringPoll.validate,
}
for name, validation := range nameToValidation {
@@ -85,6 +87,7 @@ func (s *Settings) copy() (copied Settings) {
Version: s.Version.copy(),
VPN: s.VPN.Copy(),
Pprof: s.Pprof.Copy(),
BoringPoll: s.BoringPoll.Copy(),
}
}
@@ -106,6 +109,7 @@ func (s *Settings) OverrideWith(other Settings,
patchedSettings.Version.overrideWith(other.Version)
patchedSettings.VPN.OverrideWith(other.VPN)
patchedSettings.Pprof.OverrideWith(other.Pprof)
patchedSettings.BoringPoll.overrideWith(other.BoringPoll)
err = patchedSettings.Validate(filterChoicesGetter, ipv6Supported, warner)
if err != nil {
return err
@@ -129,6 +133,7 @@ func (s *Settings) SetDefaults() {
s.VPN.setDefaults()
s.Updater.SetDefaults(s.VPN.Provider.Name)
s.Pprof.SetDefaults()
s.BoringPoll.setDefaults()
}
func (s Settings) String() string {
@@ -152,6 +157,7 @@ func (s Settings) toLinesNode() (node *gotree.Node) {
node.AppendNode(s.Updater.toLinesNode())
node.AppendNode(s.Version.toLinesNode())
node.AppendNode(s.Pprof.ToLinesNode())
node.AppendNode(s.BoringPoll.toLinesNode())
return node
}
@@ -209,6 +215,7 @@ func (s *Settings) Read(r *reader.Reader, warner Warner) (err error) {
"version": s.Version.read,
"VPN": s.VPN.read,
"profiling": s.Pprof.Read,
"boring poll": s.BoringPoll.read,
}
for name, read := range readFunctions {
+5
View File
@@ -25,4 +25,9 @@ func (l *Loop) cleanup() {
l.logger.Error("stopping port forwarding: " + err.Error())
}
}
err = l.boringPoll.Stop()
if err != nil {
l.logger.Error("stopping boring poll: " + err.Error())
}
}
+5
View File
@@ -115,3 +115,8 @@ type HealthChecker interface {
type HealthServer interface {
SetError(err error)
}
type Service interface {
Start() (runError <-chan error, err error)
Stop() error
}
+5 -3
View File
@@ -33,6 +33,7 @@ type Loop struct {
portForward PortForward
publicip PublicIPLoop
dnsLooper DNSLoop
boringPoll Service
// Other objects
starter CmdStarter // for OpenVPN
logger log.LoggerInterface
@@ -52,9 +53,9 @@ const (
)
func NewLoop(vpnSettings settings.VPN, ipv6Supported bool, vpnInputPorts []uint16,
providers Providers, storage Storage, healthSettings settings.Health,
healthChecker HealthChecker, healthServer HealthServer, openvpnConf OpenVPN,
netLinker NetLinker, fw Firewall, routing Routing,
providers Providers, storage Storage, boringPoll Service,
healthSettings settings.Health, healthChecker HealthChecker, healthServer HealthServer,
openvpnConf OpenVPN, netLinker NetLinker, fw Firewall, routing Routing,
portForward PortForward, starter CmdStarter,
publicip PublicIPLoop, dnsLooper DNSLoop,
logger log.LoggerInterface, client *http.Client,
@@ -80,6 +81,7 @@ func NewLoop(vpnSettings settings.VPN, ipv6Supported bool, vpnInputPorts []uint1
versionInfo: versionInfo,
ipv6Supported: ipv6Supported,
vpnInputPorts: vpnInputPorts,
boringPoll: boringPoll,
openvpnConf: openvpnConf,
netLinker: netLinker,
fw: fw,
+5
View File
@@ -119,6 +119,11 @@ func (l *Loop) onTunnelUp(ctx, loopCtx context.Context, data tunnelUpData) {
if err != nil {
l.logger.Error(err.Error())
}
_, err = l.boringPoll.Start()
if err != nil {
l.logger.Error("cannot start boring poll: " + err.Error())
}
}
func (l *Loop) collectHealthErrors(ctx, loopCtx context.Context, healthErrCh <-chan error) {