Skip to content

Commit 457e559

Browse files
committed
feat(others): optional BORINGPOLL_GLUETUNCOM to fight AI slop scammy gluetun[dot]com
1 parent 2460b56 commit 457e559

12 files changed

Lines changed: 287 additions & 8 deletions

File tree

.github/workflows/closed-issue.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,8 @@ jobs:
1818
If you think this is *still unresolved* and have **more information** to bring, please either
1919
re-open this issue or create another issue.
2020
21+
❤️😠 temporarily help the Gluetun community and fight the AI slop scam website `gluetun[dot]com` by setting `BORINGPOLL_GLUETUNCOM=on` on the latest image.
22+
See [the option in the wiki for more information](https://github.com/qdm12/gluetun-wiki/blob/main/setup/options/others.md)
23+
2124
This is an automated comment setup because @qdm12 is the sole maintainer of this project
2225
which became too popular to monitor closed issues.

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ ENV VPN_SERVICE_PROVIDER=pia \
231231
PPROF_HTTP_SERVER_ADDRESS=":6060" \
232232
# Extras
233233
VERSION_INFORMATION=on \
234+
BORINGPOLL_GLUETUNCOM=off \
234235
TZ= \
235236
PUID=1000 \
236237
PGID=1000

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
⚠️ This and [gluetun-wiki](https://github.com/qdm12/gluetun-wiki) are the only websites for Gluetun, other websites claiming to be official are scams ⚠️
44

5+
💁 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.
6+
57
Lightweight swiss-army-knife-like VPN client to multiple VPN service providers
68

79
![Title image](https://raw.githubusercontent.com/qdm12/gluetun/master/title.svg)

cmd/gluetun/main.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717

1818
_ "github.com/breml/rootcerts"
1919
"github.com/qdm12/gluetun/internal/alpine"
20+
"github.com/qdm12/gluetun/internal/boringpoll"
2021
"github.com/qdm12/gluetun/internal/cli"
2122
"github.com/qdm12/gluetun/internal/command"
2223
"github.com/qdm12/gluetun/internal/configuration/settings"
@@ -168,7 +169,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
168169

169170
defer fmt.Println(gluetunLogo)
170171

171-
announcementExp, err := time.Parse(time.RFC3339, "2026-04-01T00:00:00Z")
172+
announcementExp, err := time.Parse(time.RFC3339, "2026-04-30T00:00:00Z")
172173
if err != nil {
173174
return err
174175
}
@@ -179,7 +180,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
179180
Version: buildInfo.Version,
180181
Commit: buildInfo.Commit,
181182
Created: buildInfo.Created,
182-
Announcement: "All control server routes are now private by default",
183+
Announcement: "Set BORINGPOLL_GLUETUNCOM=on to help combat AI slop and shutdown that scam website",
183184
AnnounceExp: announcementExp,
184185
// Sponsor information
185186
PaypalUser: "qmcgaw",
@@ -441,11 +442,14 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
441442
httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(),
442443
openvpnFileExtractor, allSettings.Updater)
443444

445+
boringPollLogger := logger.New(log.SetComponent("boring poll"))
446+
boringPoll := boringpoll.New(httpClient, boringPollLogger, allSettings.BoringPoll)
447+
444448
vpnLogger := logger.New(log.SetComponent("vpn"))
445449
vpnLooper := vpn.NewLoop(allSettings.VPN, ipv6Supported, allSettings.Firewall.VPNInputPorts,
446-
providers, storage, allSettings.Health, healthChecker, healthcheckServer, ovpnConf, netLinker, firewallConf,
447-
routingConf, portForwardLooper, cmder, publicIPLooper, dnsLooper, vpnLogger, httpClient,
448-
buildInfo, *allSettings.Version.Enabled)
450+
providers, storage, boringPoll, allSettings.Health, healthChecker, healthcheckServer,
451+
ovpnConf, netLinker, firewallConf, routingConf, portForwardLooper, cmder, publicIPLooper,
452+
dnsLooper, vpnLogger, httpClient, buildInfo, *allSettings.Version.Enabled)
449453
vpnHandler, vpnCtx, vpnDone := goshutdown.NewGoRoutineHandler(
450454
"vpn", goroutine.OptionTimeout(time.Second))
451455
go vpnLooper.Run(vpnCtx, vpnDone)

internal/boringpoll/boringpoll.go

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
package boringpoll
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"math/rand"
8+
"net/http"
9+
"sync"
10+
"time"
11+
12+
"github.com/qdm12/gluetun/internal/configuration/settings"
13+
)
14+
15+
type BoringPoll struct {
16+
// Injected dependencies
17+
client *http.Client
18+
logger Logger
19+
20+
// Internal state
21+
urlToData map[string]*urlData
22+
23+
// Internal signals and channels
24+
cancel context.CancelFunc
25+
done <-chan struct{}
26+
mutex sync.Mutex
27+
}
28+
29+
type urlData struct{}
30+
31+
func New(client *http.Client, logger Logger, settings settings.BoringPoll) *BoringPoll {
32+
urlToData := make(map[string]*urlData)
33+
if *settings.GluetunCom {
34+
urlToData["https://gluetun.com/wp-json"] = &urlData{}
35+
}
36+
return &BoringPoll{
37+
client: client,
38+
logger: logger,
39+
urlToData: urlToData,
40+
}
41+
}
42+
43+
func (b *BoringPoll) Start() (runError <-chan error, err error) {
44+
b.mutex.Lock()
45+
defer b.mutex.Unlock()
46+
47+
if len(b.urlToData) == 0 {
48+
return nil, nil //nolint:nilnil
49+
}
50+
51+
const minPeriod = time.Minute
52+
const maxPeriod = 5 * time.Minute
53+
const logEveryBytes = 100 * 1000 * 1000 // 100 IEC MB
54+
55+
var ready, done sync.WaitGroup
56+
ready.Add(len(b.urlToData))
57+
done.Add(len(b.urlToData))
58+
ctx, cancel := context.WithCancel(context.Background())
59+
b.cancel = cancel
60+
for url := range b.urlToData {
61+
go func(url string) {
62+
defer done.Done()
63+
64+
b.logger.Infof("running against %s periodically between %s and %s "+
65+
"and will log every %s downloaded",
66+
url, minPeriod, maxPeriod, byteCountSI(logEveryBytes))
67+
totalDownloaded := uint64(0)
68+
lastDownloaded := uint64(0)
69+
consecutiveFails := 0
70+
const maxConsecutiveErrs = 3
71+
const coolDownTimeout = time.Hour
72+
timer := time.NewTimer(time.Hour)
73+
var err error
74+
75+
ready.Done()
76+
for {
77+
timeout := minPeriod + time.Duration(rand.Int63n(int64(maxPeriod-minPeriod))) //nolint:gosec
78+
if consecutiveFails >= maxConsecutiveErrs {
79+
b.logger.Debugf("pausing poll to %s for %s due to %d consecutive errors, last error: %s",
80+
url, coolDownTimeout, consecutiveFails, err)
81+
timeout = coolDownTimeout
82+
}
83+
timer.Reset(timeout)
84+
select {
85+
case <-ctx.Done():
86+
timer.Stop()
87+
totalDownloaded += lastDownloaded
88+
if totalDownloaded > 0 {
89+
b.logger.Infof("stopping poll to %s, downloaded %s!", url, byteCountSI(totalDownloaded))
90+
}
91+
return
92+
case <-timer.C:
93+
}
94+
95+
n, err := fetchURL(ctx, b.client, url)
96+
if err != nil {
97+
consecutiveFails++
98+
continue
99+
}
100+
consecutiveFails = 0
101+
totalDownloaded += uint64(n) //nolint:gosec
102+
lastDownloaded += uint64(n) //nolint:gosec
103+
if lastDownloaded >= logEveryBytes {
104+
b.logger.Infof("thanks for helping! Downloaded %s from %s!",
105+
byteCountSI(totalDownloaded), url)
106+
lastDownloaded = 0
107+
}
108+
}
109+
}(url)
110+
}
111+
return nil, nil //nolint:nilnil
112+
}
113+
114+
func fetchURL(ctx context.Context, client *http.Client, url string) (downloaded int64, err error) {
115+
const requestTimeout = 10 * time.Second
116+
ctx, cancel := context.WithTimeout(ctx, requestTimeout)
117+
defer cancel()
118+
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
119+
if err != nil {
120+
cancel()
121+
return 0, err
122+
}
123+
request.Header.Set("Cache-Control", "no-cache, no-store, must-revalidate")
124+
request.Header.Set("Pragma", "no-cache")
125+
request.Header.Set("Expires", "0")
126+
request.Header.Set("User-Agent", getRandomUserAgent())
127+
128+
response, err := client.Do(request)
129+
if err != nil {
130+
return 0, err
131+
}
132+
downloaded, err = io.Copy(io.Discard, response.Body)
133+
_ = response.Body.Close()
134+
if err != nil {
135+
return 0, err
136+
}
137+
return downloaded, nil
138+
}
139+
140+
func getRandomUserAgent() string {
141+
//nolint:lll
142+
userAgents := [...]string{
143+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
144+
"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",
145+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
146+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0",
147+
"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",
148+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/121.0.0.0 Safari/537.36",
149+
"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",
150+
"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",
151+
"Mozilla/5.0 (Android 14; Mobile; rv:122.0) Gecko/122.0 Firefox/122.0",
152+
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Mobile Safari/537.36",
153+
"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",
154+
"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",
155+
"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)",
156+
"Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)",
157+
}
158+
return userAgents[rand.Intn(len(userAgents))] //nolint:gosec
159+
}
160+
161+
func (b *BoringPoll) Stop() error {
162+
b.mutex.Lock()
163+
defer b.mutex.Unlock()
164+
165+
if b.cancel == nil {
166+
return nil
167+
}
168+
b.cancel()
169+
<-b.done
170+
b.cancel = nil
171+
b.done = nil
172+
return nil
173+
}
174+
175+
func byteCountSI(b uint64) string {
176+
const unit = 1000
177+
if b < unit {
178+
return fmt.Sprintf("%dB", b)
179+
}
180+
181+
div, exp := uint64(unit), 0
182+
for n := b / unit; n >= unit; n /= unit {
183+
div *= unit
184+
exp++
185+
}
186+
187+
return fmt.Sprintf("%.1f%cB", float64(b)/float64(div), "kMGTPE"[exp])
188+
}

internal/boringpoll/interfaces.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package boringpoll
2+
3+
type Logger interface {
4+
Infof(format string, args ...any)
5+
Debugf(format string, args ...any)
6+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package settings
2+
3+
import (
4+
"github.com/qdm12/gosettings"
5+
"github.com/qdm12/gosettings/reader"
6+
"github.com/qdm12/gotree"
7+
)
8+
9+
type BoringPoll struct {
10+
GluetunCom *bool
11+
}
12+
13+
func (b BoringPoll) validate() error {
14+
return nil
15+
}
16+
17+
func (b BoringPoll) Copy() BoringPoll {
18+
return BoringPoll{
19+
GluetunCom: gosettings.CopyPointer(b.GluetunCom),
20+
}
21+
}
22+
23+
func (b *BoringPoll) overrideWith(other BoringPoll) {
24+
b.GluetunCom = gosettings.OverrideWithPointer(b.GluetunCom, other.GluetunCom)
25+
}
26+
27+
func (b *BoringPoll) setDefaults() {
28+
b.GluetunCom = gosettings.DefaultPointer(b.GluetunCom, false)
29+
}
30+
31+
func (b BoringPoll) String() string {
32+
return b.toLinesNode().String()
33+
}
34+
35+
func (b BoringPoll) toLinesNode() *gotree.Node {
36+
if !*b.GluetunCom {
37+
return nil
38+
}
39+
40+
node := gotree.New("Boring-poll settings:")
41+
node.Append("gluetun.com: on")
42+
return node
43+
}
44+
45+
func (b *BoringPoll) read(r *reader.Reader) (err error) {
46+
b.GluetunCom, err = r.BoolPtr("BORINGPOLL_GLUETUNCOM")
47+
if err != nil {
48+
return err
49+
}
50+
return nil
51+
}

internal/configuration/settings/settings.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type Settings struct {
2828
Version Version
2929
VPN VPN
3030
Pprof pprof.Settings
31+
BoringPoll BoringPoll
3132
}
3233

3334
type FilterChoicesGetter interface {
@@ -57,6 +58,7 @@ func (s *Settings) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Support
5758
"VPN": func() error {
5859
return s.VPN.Validate(filterChoicesGetter, ipv6Supported, warner)
5960
},
61+
"boring poll": s.BoringPoll.validate,
6062
}
6163

6264
for name, validation := range nameToValidation {
@@ -85,6 +87,7 @@ func (s *Settings) copy() (copied Settings) {
8587
Version: s.Version.copy(),
8688
VPN: s.VPN.Copy(),
8789
Pprof: s.Pprof.Copy(),
90+
BoringPoll: s.BoringPoll.Copy(),
8891
}
8992
}
9093

@@ -106,6 +109,7 @@ func (s *Settings) OverrideWith(other Settings,
106109
patchedSettings.Version.overrideWith(other.Version)
107110
patchedSettings.VPN.OverrideWith(other.VPN)
108111
patchedSettings.Pprof.OverrideWith(other.Pprof)
112+
patchedSettings.BoringPoll.overrideWith(other.BoringPoll)
109113
err = patchedSettings.Validate(filterChoicesGetter, ipv6Supported, warner)
110114
if err != nil {
111115
return err
@@ -129,6 +133,7 @@ func (s *Settings) SetDefaults() {
129133
s.VPN.setDefaults()
130134
s.Updater.SetDefaults(s.VPN.Provider.Name)
131135
s.Pprof.SetDefaults()
136+
s.BoringPoll.setDefaults()
132137
}
133138

134139
func (s Settings) String() string {
@@ -152,6 +157,7 @@ func (s Settings) toLinesNode() (node *gotree.Node) {
152157
node.AppendNode(s.Updater.toLinesNode())
153158
node.AppendNode(s.Version.toLinesNode())
154159
node.AppendNode(s.Pprof.ToLinesNode())
160+
node.AppendNode(s.BoringPoll.toLinesNode())
155161

156162
return node
157163
}
@@ -209,6 +215,7 @@ func (s *Settings) Read(r *reader.Reader, warner Warner) (err error) {
209215
"version": s.Version.read,
210216
"VPN": s.VPN.read,
211217
"profiling": s.Pprof.Read,
218+
"boring poll": s.BoringPoll.read,
212219
}
213220

214221
for name, read := range readFunctions {

internal/vpn/cleanup.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,9 @@ func (l *Loop) cleanup() {
2525
l.logger.Error("stopping port forwarding: " + err.Error())
2626
}
2727
}
28+
29+
err = l.boringPoll.Stop()
30+
if err != nil {
31+
l.logger.Error("stopping boring poll: " + err.Error())
32+
}
2833
}

internal/vpn/interfaces.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,8 @@ type HealthChecker interface {
115115
type HealthServer interface {
116116
SetError(err error)
117117
}
118+
119+
type Service interface {
120+
Start() (runError <-chan error, err error)
121+
Stop() error
122+
}

0 commit comments

Comments
 (0)