mirror of
https://github.com/fosrl/gerbil.git
synced 2026-03-09 07:02:04 -05:00
Compare commits
14 Commits
dependabot
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9261b8fea | ||
|
|
c3e73d0189 | ||
|
|
df2fbdf160 | ||
|
|
cb4ac8199d | ||
|
|
dd4b86b3e5 | ||
|
|
bad290aa4e | ||
|
|
8c27d5e3bf | ||
|
|
7e7a37d49c | ||
|
|
d44aa97f32 | ||
|
|
b57ad74589 | ||
|
|
82256a3f6f | ||
|
|
9e140a94db | ||
|
|
d0c9ea5a57 | ||
|
|
c88810ef24 |
339
main.go
339
main.go
@@ -33,15 +33,16 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
interfaceName string
|
||||
listenAddr string
|
||||
mtuInt int
|
||||
lastReadings = make(map[string]PeerReading)
|
||||
mu sync.Mutex
|
||||
wgMu sync.Mutex // Protects WireGuard operations
|
||||
notifyURL string
|
||||
proxyRelay *relay.UDPProxyServer
|
||||
proxySNI *proxy.SNIProxy
|
||||
interfaceName string
|
||||
listenAddr string
|
||||
mtuInt int
|
||||
lastReadings = make(map[string]PeerReading)
|
||||
mu sync.Mutex
|
||||
wgMu sync.Mutex // Protects WireGuard operations
|
||||
notifyURL string
|
||||
proxyRelay *relay.UDPProxyServer
|
||||
proxySNI *proxy.SNIProxy
|
||||
doTrafficShaping bool
|
||||
)
|
||||
|
||||
type WgConfig struct {
|
||||
@@ -151,6 +152,7 @@ func main() {
|
||||
localOverridesStr = os.Getenv("LOCAL_OVERRIDES")
|
||||
trustedUpstreamsStr = os.Getenv("TRUSTED_UPSTREAMS")
|
||||
proxyProtocolStr := os.Getenv("PROXY_PROTOCOL")
|
||||
doTrafficShapingStr := os.Getenv("DO_TRAFFIC_SHAPING")
|
||||
|
||||
if interfaceName == "" {
|
||||
flag.StringVar(&interfaceName, "interface", "wg0", "Name of the WireGuard interface")
|
||||
@@ -222,6 +224,13 @@ func main() {
|
||||
flag.BoolVar(&proxyProtocol, "proxy-protocol", true, "Enable PROXY protocol v1 for preserving client IP")
|
||||
}
|
||||
|
||||
if doTrafficShapingStr != "" {
|
||||
doTrafficShaping = strings.ToLower(doTrafficShapingStr) == "true"
|
||||
}
|
||||
if doTrafficShapingStr == "" {
|
||||
flag.BoolVar(&doTrafficShaping, "do-traffic-shaping", false, "Whether to set up traffic shaping rules for peers (requires tc command and root privileges)")
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
|
||||
logger.Init()
|
||||
@@ -555,6 +564,10 @@ func ensureWireguardInterface(wgconfig WgConfig) error {
|
||||
logger.Warn("Failed to ensure MSS clamping: %v", err)
|
||||
}
|
||||
|
||||
if err := ensureWireguardFirewall(); err != nil {
|
||||
logger.Warn("Failed to ensure WireGuard firewall rules: %v", err)
|
||||
}
|
||||
|
||||
logger.Info("WireGuard interface %s created and configured", interfaceName)
|
||||
|
||||
return nil
|
||||
@@ -723,6 +736,113 @@ func ensureMSSClamping() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureWireguardFirewall() error {
|
||||
// Rules to enforce:
|
||||
// 1. Allow established/related connections (responses to our outbound traffic)
|
||||
// 2. Allow ICMP ping packets
|
||||
// 3. Drop all other inbound traffic from peers
|
||||
|
||||
// Define the rules we want to ensure exist
|
||||
rules := [][]string{
|
||||
// Allow established and related connections (responses to outbound traffic)
|
||||
{
|
||||
"-A", "INPUT",
|
||||
"-i", interfaceName,
|
||||
"-m", "conntrack",
|
||||
"--ctstate", "ESTABLISHED,RELATED",
|
||||
"-j", "ACCEPT",
|
||||
},
|
||||
// Allow ICMP ping requests
|
||||
{
|
||||
"-A", "INPUT",
|
||||
"-i", interfaceName,
|
||||
"-p", "icmp",
|
||||
"--icmp-type", "8",
|
||||
"-j", "ACCEPT",
|
||||
},
|
||||
// Drop all other inbound traffic from WireGuard interface
|
||||
{
|
||||
"-A", "INPUT",
|
||||
"-i", interfaceName,
|
||||
"-j", "DROP",
|
||||
},
|
||||
}
|
||||
|
||||
// First, try to delete any existing rules for this interface
|
||||
for _, rule := range rules {
|
||||
deleteArgs := make([]string, len(rule))
|
||||
copy(deleteArgs, rule)
|
||||
// Change -A to -D for deletion
|
||||
for i, arg := range deleteArgs {
|
||||
if arg == "-A" {
|
||||
deleteArgs[i] = "-D"
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
deleteCmd := exec.Command("/usr/sbin/iptables", deleteArgs...)
|
||||
logger.Debug("Attempting to delete existing firewall rule: %v", deleteArgs)
|
||||
|
||||
// Try deletion multiple times to handle multiple existing rules
|
||||
for i := 0; i < 5; i++ {
|
||||
out, err := deleteCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
logger.Debug("Deletion stopped: %v (output: %s)", exitErr.String(), string(out))
|
||||
}
|
||||
break // No more rules to delete
|
||||
}
|
||||
logger.Info("Deleted existing firewall rule (attempt %d)", i+1)
|
||||
}
|
||||
}
|
||||
|
||||
// Now add the rules
|
||||
var errors []error
|
||||
for i, rule := range rules {
|
||||
addCmd := exec.Command("/usr/sbin/iptables", rule...)
|
||||
logger.Info("Adding WireGuard firewall rule %d: %v", i+1, rule)
|
||||
|
||||
if out, err := addCmd.CombinedOutput(); err != nil {
|
||||
errMsg := fmt.Sprintf("Failed to add firewall rule %d: %v (output: %s)", i+1, err, string(out))
|
||||
logger.Error("%s", errMsg)
|
||||
errors = append(errors, fmt.Errorf("%s", errMsg))
|
||||
continue
|
||||
}
|
||||
|
||||
// Verify the rule was added by checking
|
||||
checkArgs := make([]string, len(rule))
|
||||
copy(checkArgs, rule)
|
||||
// Change -A to -C for check
|
||||
for j, arg := range checkArgs {
|
||||
if arg == "-A" {
|
||||
checkArgs[j] = "-C"
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
checkCmd := exec.Command("/usr/sbin/iptables", checkArgs...)
|
||||
if out, err := checkCmd.CombinedOutput(); err != nil {
|
||||
errMsg := fmt.Sprintf("Rule verification failed for rule %d: %v (output: %s)", i+1, err, string(out))
|
||||
logger.Error("%s", errMsg)
|
||||
errors = append(errors, fmt.Errorf("%s", errMsg))
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Info("Successfully added and verified WireGuard firewall rule %d", i+1)
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
var errMsgs []string
|
||||
for _, err := range errors {
|
||||
errMsgs = append(errMsgs, err.Error())
|
||||
}
|
||||
return fmt.Errorf("WireGuard firewall setup encountered errors:\n%s", strings.Join(errMsgs, "\n"))
|
||||
}
|
||||
|
||||
logger.Info("WireGuard firewall rules successfully configured for interface %s", interfaceName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func handlePeer(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodPost:
|
||||
@@ -775,17 +895,23 @@ func addPeerInternal(peer Peer) error {
|
||||
return fmt.Errorf("failed to parse public key: %v", err)
|
||||
}
|
||||
|
||||
logger.Debug("Adding peer %s with AllowedIPs: %v", peer.PublicKey, peer.AllowedIPs)
|
||||
|
||||
// parse allowed IPs into array of net.IPNet
|
||||
var allowedIPs []net.IPNet
|
||||
var wgIPs []string
|
||||
for _, ipStr := range peer.AllowedIPs {
|
||||
logger.Debug("Parsing AllowedIP: %s", ipStr)
|
||||
_, ipNet, err := net.ParseCIDR(ipStr)
|
||||
if err != nil {
|
||||
logger.Warn("Failed to parse allowed IP '%s' for peer %s: %v", ipStr, peer.PublicKey, err)
|
||||
return fmt.Errorf("failed to parse allowed IP: %v", err)
|
||||
}
|
||||
allowedIPs = append(allowedIPs, *ipNet)
|
||||
// Extract the IP address from the CIDR for relay cleanup
|
||||
wgIPs = append(wgIPs, ipNet.IP.String())
|
||||
extractedIP := ipNet.IP.String()
|
||||
wgIPs = append(wgIPs, extractedIP)
|
||||
logger.Debug("Extracted IP %s from AllowedIP %s", extractedIP, ipStr)
|
||||
}
|
||||
|
||||
peerConfig := wgtypes.PeerConfig{
|
||||
@@ -801,6 +927,18 @@ func addPeerInternal(peer Peer) error {
|
||||
return fmt.Errorf("failed to add peer: %v", err)
|
||||
}
|
||||
|
||||
// Setup bandwidth limiting for each peer IP
|
||||
if doTrafficShaping {
|
||||
logger.Debug("doTrafficShaping is true, setting up bandwidth limits for %d IPs", len(wgIPs))
|
||||
for _, wgIP := range wgIPs {
|
||||
if err := setupPeerBandwidthLimit(wgIP); err != nil {
|
||||
logger.Warn("Failed to setup bandwidth limit for peer IP %s: %v", wgIP, err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.Debug("doTrafficShaping is false, skipping bandwidth limit setup")
|
||||
}
|
||||
|
||||
// Clear relay connections for the peer's WireGuard IPs
|
||||
if proxyRelay != nil {
|
||||
for _, wgIP := range wgIPs {
|
||||
@@ -845,19 +983,17 @@ func removePeerInternal(publicKey string) error {
|
||||
return fmt.Errorf("failed to parse public key: %v", err)
|
||||
}
|
||||
|
||||
// Get current peer info before removing to clear relay connections
|
||||
// Get current peer info before removing to clear relay connections and bandwidth limits
|
||||
var wgIPs []string
|
||||
if proxyRelay != nil {
|
||||
device, err := wgClient.Device(interfaceName)
|
||||
if err == nil {
|
||||
for _, peer := range device.Peers {
|
||||
if peer.PublicKey.String() == publicKey {
|
||||
// Extract WireGuard IPs from this peer's allowed IPs
|
||||
for _, allowedIP := range peer.AllowedIPs {
|
||||
wgIPs = append(wgIPs, allowedIP.IP.String())
|
||||
}
|
||||
break
|
||||
device, err := wgClient.Device(interfaceName)
|
||||
if err == nil {
|
||||
for _, peer := range device.Peers {
|
||||
if peer.PublicKey.String() == publicKey {
|
||||
// Extract WireGuard IPs from this peer's allowed IPs
|
||||
for _, allowedIP := range peer.AllowedIPs {
|
||||
wgIPs = append(wgIPs, allowedIP.IP.String())
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -875,6 +1011,15 @@ func removePeerInternal(publicKey string) error {
|
||||
return fmt.Errorf("failed to remove peer: %v", err)
|
||||
}
|
||||
|
||||
// Remove bandwidth limits for each peer IP
|
||||
if doTrafficShaping {
|
||||
for _, wgIP := range wgIPs {
|
||||
if err := removePeerBandwidthLimit(wgIP); err != nil {
|
||||
logger.Warn("Failed to remove bandwidth limit for peer IP %s: %v", wgIP, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear relay connections for the peer's WireGuard IPs
|
||||
if proxyRelay != nil {
|
||||
for _, wgIP := range wgIPs {
|
||||
@@ -1204,3 +1349,155 @@ func monitorMemory(limit uint64) {
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// setupPeerBandwidthLimit sets up TC (Traffic Control) to limit bandwidth for a specific peer IP
|
||||
// Currently hardcoded to 20 Mbps per peer
|
||||
func setupPeerBandwidthLimit(peerIP string) error {
|
||||
logger.Debug("setupPeerBandwidthLimit called for peer IP: %s", peerIP)
|
||||
const bandwidthLimit = "50mbit" // 50 Mbps limit per peer
|
||||
|
||||
// Parse the IP to get just the IP address (strip any CIDR notation if present)
|
||||
ip := peerIP
|
||||
if strings.Contains(peerIP, "/") {
|
||||
parsedIP, _, err := net.ParseCIDR(peerIP)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse peer IP: %v", err)
|
||||
}
|
||||
ip = parsedIP.String()
|
||||
}
|
||||
|
||||
// First, ensure we have a root qdisc on the interface (HTB - Hierarchical Token Bucket)
|
||||
// Check if qdisc already exists
|
||||
cmd := exec.Command("tc", "qdisc", "show", "dev", interfaceName)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check qdisc: %v, output: %s", err, string(output))
|
||||
}
|
||||
|
||||
// If no HTB qdisc exists, create one
|
||||
if !strings.Contains(string(output), "htb") {
|
||||
cmd = exec.Command("tc", "qdisc", "add", "dev", interfaceName, "root", "handle", "1:", "htb", "default", "9999")
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to add root qdisc: %v, output: %s", err, string(output))
|
||||
}
|
||||
logger.Info("Created HTB root qdisc on %s", interfaceName)
|
||||
}
|
||||
|
||||
// Generate a unique class ID based on the IP address
|
||||
// We'll use the last octet of the IP as part of the class ID
|
||||
ipParts := strings.Split(ip, ".")
|
||||
if len(ipParts) != 4 {
|
||||
return fmt.Errorf("invalid IPv4 address: %s", ip)
|
||||
}
|
||||
lastOctet := ipParts[3]
|
||||
classID := fmt.Sprintf("1:%s", lastOctet)
|
||||
logger.Debug("Generated class ID %s for peer IP %s", classID, ip)
|
||||
|
||||
// Create a class for this peer with bandwidth limit
|
||||
cmd = exec.Command("tc", "class", "add", "dev", interfaceName, "parent", "1:", "classid", classID,
|
||||
"htb", "rate", bandwidthLimit, "ceil", bandwidthLimit)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
logger.Debug("tc class add failed for %s: %v, output: %s", ip, err, string(output))
|
||||
// If class already exists, try to replace it
|
||||
if strings.Contains(string(output), "File exists") {
|
||||
cmd = exec.Command("tc", "class", "replace", "dev", interfaceName, "parent", "1:", "classid", classID,
|
||||
"htb", "rate", bandwidthLimit, "ceil", bandwidthLimit)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to replace class: %v, output: %s", err, string(output))
|
||||
}
|
||||
logger.Debug("Successfully replaced existing class %s for peer IP %s", classID, ip)
|
||||
} else {
|
||||
return fmt.Errorf("failed to add class: %v, output: %s", err, string(output))
|
||||
}
|
||||
} else {
|
||||
logger.Debug("Successfully added new class %s for peer IP %s", classID, ip)
|
||||
}
|
||||
|
||||
// Add a filter to match traffic from this peer IP (ingress)
|
||||
cmd = exec.Command("tc", "filter", "add", "dev", interfaceName, "protocol", "ip", "parent", "1:",
|
||||
"prio", "1", "u32", "match", "ip", "src", ip, "flowid", classID)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
// If filter fails, log but don't fail the peer addition
|
||||
logger.Warn("Failed to add ingress filter for peer IP %s: %v, output: %s", ip, err, string(output))
|
||||
}
|
||||
|
||||
// Add a filter to match traffic to this peer IP (egress)
|
||||
cmd = exec.Command("tc", "filter", "add", "dev", interfaceName, "protocol", "ip", "parent", "1:",
|
||||
"prio", "1", "u32", "match", "ip", "dst", ip, "flowid", classID)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
// If filter fails, log but don't fail the peer addition
|
||||
logger.Warn("Failed to add egress filter for peer IP %s: %v, output: %s", ip, err, string(output))
|
||||
}
|
||||
|
||||
logger.Info("Setup bandwidth limit of %s for peer IP %s (class %s)", bandwidthLimit, ip, classID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// removePeerBandwidthLimit removes TC rules for a specific peer IP
|
||||
func removePeerBandwidthLimit(peerIP string) error {
|
||||
// Parse the IP to get just the IP address
|
||||
ip := peerIP
|
||||
if strings.Contains(peerIP, "/") {
|
||||
parsedIP, _, err := net.ParseCIDR(peerIP)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse peer IP: %v", err)
|
||||
}
|
||||
ip = parsedIP.String()
|
||||
}
|
||||
|
||||
// Generate the class ID based on the IP
|
||||
ipParts := strings.Split(ip, ".")
|
||||
if len(ipParts) != 4 {
|
||||
return fmt.Errorf("invalid IPv4 address: %s", ip)
|
||||
}
|
||||
lastOctet := ipParts[3]
|
||||
classID := fmt.Sprintf("1:%s", lastOctet)
|
||||
|
||||
// Remove filters for this IP
|
||||
// List all filters to find the ones for this class
|
||||
cmd := exec.Command("tc", "filter", "show", "dev", interfaceName, "parent", "1:")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
logger.Warn("Failed to list filters for peer IP %s: %v, output: %s", ip, err, string(output))
|
||||
} else {
|
||||
// Parse the output to find filter handles that match this classID
|
||||
// The output format includes lines like:
|
||||
// filter parent 1: protocol ip pref 1 u32 chain 0 fh 800::800 order 2048 key ht 800 bkt 0 flowid 1:4
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
// Look for lines containing our flowid (classID)
|
||||
if strings.Contains(line, "flowid "+classID) && strings.Contains(line, "fh ") {
|
||||
// Extract handle (format: fh 800::800)
|
||||
parts := strings.Fields(line)
|
||||
var handle string
|
||||
for j, part := range parts {
|
||||
if part == "fh" && j+1 < len(parts) {
|
||||
handle = parts[j+1]
|
||||
break
|
||||
}
|
||||
}
|
||||
if handle != "" {
|
||||
// Delete this filter using the handle
|
||||
delCmd := exec.Command("tc", "filter", "del", "dev", interfaceName, "parent", "1:", "handle", handle, "prio", "1", "u32")
|
||||
if delOutput, delErr := delCmd.CombinedOutput(); delErr != nil {
|
||||
logger.Debug("Failed to delete filter handle %s for peer IP %s: %v, output: %s", handle, ip, delErr, string(delOutput))
|
||||
} else {
|
||||
logger.Debug("Deleted filter handle %s for peer IP %s", handle, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the class
|
||||
cmd = exec.Command("tc", "class", "del", "dev", interfaceName, "classid", classID)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
// It's okay if the class doesn't exist
|
||||
if !strings.Contains(string(output), "No such file or directory") && !strings.Contains(string(output), "Cannot find") {
|
||||
logger.Warn("Failed to remove class for peer IP %s: %v, output: %s", ip, err, string(output))
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("Removed bandwidth limit for peer IP %s (class %s)", ip, classID)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -839,7 +839,7 @@ func (s *UDPProxyServer) clearSessionsForIP(ip string) {
|
||||
s.wgSessions.Delete(key)
|
||||
}
|
||||
|
||||
logger.Info("Cleared %d sessions for WG IP: %s", len(keysToDelete), ip)
|
||||
logger.Debug("Cleared %d sessions for WG IP: %s", len(keysToDelete), ip)
|
||||
}
|
||||
|
||||
// // clearProxyMappingsForWGIP removes all proxy mappings that have destinations pointing to a specific WireGuard IP
|
||||
|
||||
Reference in New Issue
Block a user