Files
ihasvpn/internal/netcfg/sysctl.go
T
jcoffey-dev 02e7993c87 Rename the project to ihasvpn
WGX shares its name with several other WireGuard tools, so the project
becomes ihasvpn, alongside ihasmail.

- Module github.com/Coffey-Labs/ihasvpn, command cmd/ihasvpn, image
  ghcr.io/coffey-labs/ihasvpn.
- Environment variables move from WGX_* to IHASVPN_*. The default database
  is ihasvpn.db, the nftables table is `ihasvpn`, metrics are ihasvpn_*, and
  the session cookie and theme key are renamed, so existing sessions end.
- The mark is the ihasmail cat peeking over the edge of a shield, drawn as
  a vector. docs/brand/generate.py builds the mark, mono mark, wordmarks,
  social card, favicons and app icons from that one drawing.
- The console takes ihasmail's palette: the ihasmail.org teal-navy for dark,
  its contrast-checked light tiers with the site's light accent, received
  traffic in the cat's orange and sent in teal. The wordmark weight and
  font stack follow ihasmail.org.
- Detail values wrap at spaces before breaking inside an address, so an
  IPv6 tunnel address no longer splits mid-number.
- The README history note about the earlier WGX installer is gone with the
  name it explained. Screenshots retaken.
2026-09-12 23:48:36 -07:00

95 lines
3.8 KiB
Go

package netcfg
import (
"fmt"
"os"
"path/filepath"
"strings"
)
// Sysctl is one kernel parameter and the value ihasvpn wants for it.
type Sysctl struct {
Key string
Value string
// Required marks the ones the VPN cannot work without (forwarding). The
// others are throughput tuning: nice to have, and often refused inside a
// container because they are not network-namespaced.
Required bool
// Why is shown in the log and the UI when a value could not be set.
Why string
}
// Result records what happened to one sysctl.
type Result struct {
Sysctl
Applied bool
Current string
Err string
}
// Wanted returns the sysctls ihasvpn applies at startup, in order.
func Wanted(ipv6 bool) []Sysctl {
s := []Sysctl{
{Key: "net.ipv4.ip_forward", Value: "1", Required: true, Why: "peers cannot reach anything beyond the server without forwarding"},
// Strict reverse-path filtering drops replies that arrive on the
// tunnel for a source the kernel would route elsewhere. Loose is
// what every VPN gateway runs.
{Key: "net.ipv4.conf.all.rp_filter", Value: "2", Why: "strict rp_filter drops legitimate tunnel replies"},
{Key: "net.ipv4.conf.default.rp_filter", Value: "2", Why: "strict rp_filter drops legitimate tunnel replies"},
// The remaining ones are throughput. They are global (not
// namespaced), so inside a container they usually fail and must be
// set on the host instead -- see docs/performance.md.
{Key: "net.core.rmem_max", Value: "26214400", Why: "larger UDP receive buffers stop bursts being dropped before WireGuard reads them"},
{Key: "net.core.wmem_max", Value: "26214400", Why: "larger UDP send buffers keep the encrypt path from stalling"},
{Key: "net.core.rmem_default", Value: "1048576", Why: "default socket receive buffer"},
{Key: "net.core.wmem_default", Value: "1048576", Why: "default socket send buffer"},
{Key: "net.core.netdev_max_backlog", Value: "16384", Why: "deeper per-CPU input queue for 10GbE bursts"},
{Key: "net.ipv4.udp_rmem_min", Value: "16384", Why: "minimum UDP receive buffer under memory pressure"},
{Key: "net.ipv4.udp_wmem_min", Value: "16384", Why: "minimum UDP send buffer under memory pressure"},
}
if ipv6 {
s = append(s, Sysctl{Key: "net.ipv6.conf.all.forwarding", Value: "1", Required: true, Why: "IPv6 peers cannot reach anything beyond the server without forwarding"})
}
return s
}
// ApplyAll writes each sysctl through /proc/sys and reports what happened.
// A value that is already right counts as applied. A required value that
// cannot be set is returned as an error along with the full report so the
// caller can decide whether to keep going.
func ApplyAll(want []Sysctl) ([]Result, error) {
var results []Result
var fatal []string
for _, s := range want {
r := Result{Sysctl: s}
path := filepath.Join("/proc/sys", strings.ReplaceAll(s.Key, ".", "/"))
cur, err := os.ReadFile(path)
if err == nil {
r.Current = strings.TrimSpace(string(cur))
}
if r.Current == s.Value {
r.Applied = true
results = append(results, r)
continue
}
if err := os.WriteFile(path, []byte(s.Value), 0o644); err != nil {
r.Err = err.Error()
if s.Required && !forwardingSatisfied(r.Current, s.Value) {
fatal = append(fatal, fmt.Sprintf("%s=%s (%s)", s.Key, s.Value, r.Err))
}
} else {
r.Applied = true
r.Current = s.Value
}
results = append(results, r)
}
if len(fatal) > 0 {
return results, fmt.Errorf("required sysctls could not be set: %s -- pass them with `--sysctl` or the compose `sysctls:` list", strings.Join(fatal, ", "))
}
return results, nil
}
// forwardingSatisfied treats "1" as satisfied for forwarding keys even when
// the file was read-only, which is what a compose `sysctls:` entry produces.
func forwardingSatisfied(current, want string) bool { return current == want }