25 Commits

Author SHA1 Message Date
Owen
cba4dc646d Try to setup qemu 2025-03-03 23:58:55 -05:00
Owen
88be6d133d Update upload action to v4 2025-03-03 22:38:49 -05:00
Owen Schwartz
34a80c6411 Merge pull request #8 from fosrl/dev
Add CICD
2025-03-03 22:37:29 -05:00
Owen
6565fdbe62 Fix merge issue 2025-03-03 22:36:58 -05:00
Milo Schwartz
9c5ddcdfb8 Merge branch 'dev' of https://github.com/fosrl/gerbil into dev 2025-01-29 22:26:02 -05:00
Milo Schwartz
32176c74a0 add cicd 2025-01-29 22:25:33 -05:00
Owen Schwartz
aa4f4ebfab Merge pull request #5 from fosrl/dev
MSS Clamping & Retry Remote Config
2025-01-19 17:27:52 -05:00
Owen Schwartz
bab8630756 Add retry to config request 2025-01-19 17:26:15 -05:00
Owen Schwartz
24e993ee41 Add mss clamping 2025-01-19 17:19:09 -05:00
Owen Schwartz
5d4faaff65 Standarize release build makefile 2025-01-16 07:41:27 -05:00
Owen Schwartz
c04a6ecfd7 Merge pull request #4 from fosrl/dev
MTU Set to 1280 and Env Var Support
2025-01-15 22:37:04 -05:00
Owen Schwartz
bc69b625fa Better feedback about config 2025-01-15 21:55:18 -05:00
Owen Schwartz
1712b88e18 MTU 1280 by default 2025-01-15 20:54:24 -05:00
Owen Schwartz
fc451baa06 Mtu flag working 2025-01-15 00:01:34 -05:00
Owen Schwartz
fcb3b5de6a Merge branch 'env-vars' into dev 2025-01-14 23:45:12 -05:00
Owen Schwartz
5101e6f125 Add env vars 2025-01-14 22:04:06 -05:00
Owen Schwartz
217379d286 Working on mtu 2025-01-14 21:57:00 -05:00
Owen Schwartz
dc4297e6c4 Merge branch 'dev' of https://github.com/fosrl/gerbil into dev 2025-01-13 22:53:41 -05:00
Owen Schwartz
b7f1d09d4d Merge branch 'main' into dev 2025-01-13 22:53:03 -05:00
Owen Schwartz
b44d320968 Allow changing mtu; set default low 2025-01-13 22:52:11 -05:00
Milo Schwartz
29a0f02ceb fix typos in readme 2025-01-09 16:57:17 -05:00
Milo Schwartz
f0f63f22b4 Merge pull request #3 from fosrl/dev
add security policy
2025-01-08 21:58:11 -05:00
Milo Schwartz
91367f0e68 add security policy 2025-01-08 21:35:00 -05:00
Milo Schwartz
81a0ac576f Merge pull request #2 from fosrl/dev
update CONTRIBUTING.md
2025-01-06 22:45:30 -05:00
Milo Schwartz
ad0633c42d update CONTRIBUTING.md 2025-01-06 22:29:10 -05:00
9 changed files with 275 additions and 53 deletions

52
.github/workflows/cicd.yml vendored Normal file
View File

@@ -0,0 +1,52 @@
name: CI/CD Pipeline
on:
push:
tags:
- "*"
jobs:
release:
name: Build and Release
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Extract tag name
id: get-tag
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: 1.23.1
- name: Build and push Docker images
run: |
TAG=${{ env.TAG }}
make docker-build-release tag=$TAG
- name: Build binaries
run: |
make go-build-release
- name: Upload artifacts from /bin
uses: actions/upload-artifact@v4
with:
name: binaries
path: bin/

4
.gitignore vendored
View File

@@ -1 +1,3 @@
gerbil
gerbil
.DS_Store
bin/

View File

@@ -1,6 +1,12 @@
## Contributing
Contributions are welcome! Please see the following page in our documentation with future plans and feature ideas if you are looking for a place to start.
Contributions are welcome!
Please see the contribution and local development guide on the docs page before getting started:
https://docs.fossorial.io/development
For ideas about what features to work on and our future plans, please see the roadmap:
https://docs.fossorial.io/roadmap
@@ -15,4 +21,4 @@ By creating this pull request, I grant the project maintainers an unlimited,
perpetual license to use, modify, and redistribute these contributions under any terms they
choose, including both the AGPLv3 and the Fossorial Commercial license terms. I
represent that I have the right to grant this license for all contributed content.
```
```

View File

@@ -18,6 +18,8 @@ RUN CGO_ENABLED=0 GOOS=linux go build -o /gerbil
# Start a new stage from scratch
FROM ubuntu:22.04 AS runner
RUN apt-get update && apt-get install -y iptables iproute2 && rm -rf /var/lib/apt/lists/*
# Copy the pre-built binary file from the previous stage and the entrypoint script
COPY --from=builder /gerbil /usr/local/bin/
COPY entrypoint.sh /

View File

@@ -1,6 +1,14 @@
all: build push
docker-build-release:
@if [ -z "$(tag)" ]; then \
echo "Error: tag is required. Usage: make build-all tag=<tag>"; \
exit 1; \
fi
docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/gerbil:latest -f Dockerfile --push .
docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/gerbil:$(tag) -f Dockerfile --push .
build:
docker build -t fosrl/gerbil:latest .
@@ -13,5 +21,9 @@ test:
local:
CGO_ENABLED=0 GOOS=linux go build -o gerbil
go-build-release:
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/gerbil_linux_arm64
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/gerbil_linux_amd64
clean:
rm gerbil
rm gerbil

View File

@@ -4,7 +4,7 @@ Gerbil is a simple [WireGuard](https://www.wireguard.com/) interface management
### Installation and Documentation
Gerbil can be used stand alone with your own API, a static JSON file, or with Pangolin and Newt as part of the larger system. See documentation below:
Gerbil can be used standalone with your own API, a static JSON file, or with Pangolin and Newt as part of the larger system. See documentation below:
- [Installation Instructions](https://docs.fossorial.io)
- [Full Documentation](https://docs.fossorial.io)
@@ -19,7 +19,7 @@ _Sample output of a Gerbil container connected to Pangolin and terminating vario
### Setup WireGuard
A WireGuard interface will be created and configured on the local Linux machine or in the Docker container according to the values given in either a JSON config file or via the remote server. If the interface already exists it will be reconfigured.
A WireGuard interface will be created and configured on the local Linux machine or in the Docker container according to the values given in either a JSON config file or via the remote server. If the interface already exists, it will be reconfigured.
### Manage Peers
@@ -27,7 +27,7 @@ Gerbil will create the peers defined in the config on the WireGuard interface. T
### Report Bandwidth
Bytes transmitted in and out of each peer is collected every 10 seconds and incremental usage is reported via the "reportBandwidthTo" endpoint. This can be used to track data usage of each peer on the remote server.
Bytes transmitted in and out of each peer are collected every 10 seconds, and incremental usage is reported via the "reportBandwidthTo" endpoint. This can be used to track data usage of each peer on the remote server.
## CLI Args
@@ -97,4 +97,4 @@ Gerbil is dual licensed under the AGPLv3 and the Fossorial Commercial license. F
## Contributions
Please see [CONTRIBUTIONS](./CONTRIBUTING.md) in the repository for guidelines and best practices.
Please see [CONTRIBUTIONS](./CONTRIBUTING.md) in the repository for guidelines and best practices.

14
SECURITY.md Normal file
View File

@@ -0,0 +1,14 @@
# Security Policy
If you discover a security vulnerability, please follow the steps below to responsibly disclose it to us:
1. **Do not create a public GitHub issue or discussion post.** This could put the security of other users at risk.
2. Send a detailed report to [security@fossorial.io](mailto:security@fossorial.io) or send a **private** message to a maintainer on [Discord](https://discord.gg/HCJR8Xhme4). Include:
- Description and location of the vulnerability.
- Potential impact of the vulnerability.
- Steps to reproduce the vulnerability.
- Potential solutions to fix the vulnerability.
- Your name/handle and a link for recognition (optional).
We aim to address the issue as soon as possible.

View File

@@ -1,7 +1,5 @@
#!/bin/sh
# Sample from https://github.com/traefik/traefik-library-image/blob/5070edb25b03cca6802d75d5037576c840f73fdd/v3.1/alpine/entrypoint.sh
set -e
# first arg is `-f` or `--some-option`
@@ -9,13 +7,4 @@ if [ "${1#-}" != "$1" ]; then
set -- gerbil "$@"
fi
# if our command is a valid Gerbil subcommand, let's invoke it through Gerbil instead
# (this allows for "docker run gerbil version", etc)
if gerbil "$1" --help >/dev/null 2>&1
then
set -- gerbil "$@"
else
echo "= '$1' is not a Gerbil command: assuming shell execution." 1>&2
fi
exec "$@"

213
main.go
View File

@@ -9,6 +9,8 @@ import (
"net"
"net/http"
"os"
"os/exec"
"strconv"
"strings"
"sync"
"time"
@@ -20,8 +22,9 @@ import (
)
var (
interfaceName = "wg0"
listenAddr = ":3003"
interfaceName string
listenAddr string
mtuInt int
lastReadings = make(map[string]PeerReading)
mu sync.Mutex
)
@@ -72,52 +75,91 @@ func parseLogLevel(level string) logger.LogLevel {
}
func main() {
var err error
var wgconfig WgConfig
var (
err error
wgconfig WgConfig
configFile string
remoteConfigURL string
reportBandwidthTo string
generateAndSaveKeyTo string
reachableAt string
logLevel string
mtu string
)
// Define command line flags
interfaceNameArg := flag.String("interface", "wg0", "Name of the WireGuard interface")
configFile := flag.String("config", "", "Path to local configuration file")
remoteConfigURL := flag.String("remoteConfig", "", "URL to fetch remote configuration")
listenAddrArg := flag.String("listen", ":3003", "Address to listen on")
reportBandwidthTo := flag.String("reportBandwidthTo", "", "Address to listen on")
generateAndSaveKeyTo := flag.String("generateAndSaveKeyTo", "", "Path to save generated private key")
reachableAt := flag.String("reachableAt", "", "Endpoint of the http server to tell remote config about")
logLevel := flag.String("log-level", "INFO", "Log level (DEBUG, INFO, WARN, ERROR, FATAL)")
interfaceName = os.Getenv("INTERFACE")
configFile = os.Getenv("CONFIG")
remoteConfigURL = os.Getenv("REMOTE_CONFIG")
listenAddr = os.Getenv("LISTEN")
reportBandwidthTo = os.Getenv("REPORT_BANDWIDTH_TO")
generateAndSaveKeyTo = os.Getenv("GENERATE_AND_SAVE_KEY_TO")
reachableAt = os.Getenv("REACHABLE_AT")
logLevel = os.Getenv("LOG_LEVEL")
mtu = os.Getenv("MTU")
if interfaceName == "" {
flag.StringVar(&interfaceName, "interface", "wg0", "Name of the WireGuard interface")
}
if configFile == "" {
flag.StringVar(&configFile, "config", "", "Path to local configuration file")
}
if remoteConfigURL == "" {
flag.StringVar(&remoteConfigURL, "remoteConfig", "", "URL to fetch remote configuration")
}
if listenAddr == "" {
flag.StringVar(&listenAddr, "listen", ":3003", "Address to listen on")
}
if reportBandwidthTo == "" {
flag.StringVar(&reportBandwidthTo, "reportBandwidthTo", "", "Address to listen on")
}
if generateAndSaveKeyTo == "" {
flag.StringVar(&generateAndSaveKeyTo, "generateAndSaveKeyTo", "", "Path to save generated private key")
}
if reachableAt == "" {
flag.StringVar(&reachableAt, "reachableAt", "", "Endpoint of the http server to tell remote config about")
}
if logLevel == "" {
flag.StringVar(&logLevel, "log-level", "INFO", "Log level (DEBUG, INFO, WARN, ERROR, FATAL)")
}
if mtu == "" {
flag.StringVar(&mtu, "mtu", "1280", "MTU of the WireGuard interface")
}
flag.Parse()
logger.Init()
logger.GetLogger().SetLevel(parseLogLevel(*logLevel))
logger.GetLogger().SetLevel(parseLogLevel(logLevel))
if *interfaceNameArg != "" {
interfaceName = *interfaceNameArg
}
if *listenAddrArg != "" {
listenAddr = *listenAddrArg
mtuInt, err = strconv.Atoi(mtu)
if err != nil {
logger.Fatal("Failed to parse MTU: %v", err)
}
// Validate that only one config option is provided
if (*configFile != "" && *remoteConfigURL != "") || (*configFile == "" && *remoteConfigURL == "") {
logger.Fatal("Please provide either --config or --remoteConfig, but not both")
// are they missing either the config file or the remote config URL?
if configFile == "" && remoteConfigURL == "" {
logger.Fatal("You must provide either a config file or a remote config URL")
}
// do they have both the config file and the remote config URL?
if configFile != "" && remoteConfigURL != "" {
logger.Fatal("You must provide either a config file or a remote config URL, not both")
}
var key wgtypes.Key
// if generateAndSaveKeyTo is provided, generate a private key and save it to the file. if the file already exists, load the key from the file
if *generateAndSaveKeyTo != "" {
if _, err := os.Stat(*generateAndSaveKeyTo); os.IsNotExist(err) {
if generateAndSaveKeyTo != "" {
if _, err := os.Stat(generateAndSaveKeyTo); os.IsNotExist(err) {
// generate a new private key
key, err = wgtypes.GeneratePrivateKey()
if err != nil {
logger.Fatal("Failed to generate private key: %v", err)
}
// save the key to the file
err = os.WriteFile(*generateAndSaveKeyTo, []byte(key.String()), 0644)
err = os.WriteFile(generateAndSaveKeyTo, []byte(key.String()), 0644)
if err != nil {
logger.Fatal("Failed to save private key: %v", err)
}
} else {
keyData, err := os.ReadFile(*generateAndSaveKeyTo)
keyData, err := os.ReadFile(generateAndSaveKeyTo)
if err != nil {
logger.Fatal("Failed to read private key: %v", err)
}
@@ -138,8 +180,8 @@ func main() {
}
// Load configuration based on provided argument
if *configFile != "" {
wgconfig, err = loadConfig(*configFile)
if configFile != "" {
wgconfig, err = loadConfig(configFile)
if err != nil {
logger.Fatal("Failed to load configuration: %v", err)
}
@@ -147,11 +189,17 @@ func main() {
wgconfig.PrivateKey = key.String()
}
} else {
wgconfig, err = loadRemoteConfig(*remoteConfigURL, key, *reachableAt)
if err != nil {
logger.Fatal("Failed to load configuration: %v", err)
// loop until we get the config
for wgconfig.PrivateKey == "" {
logger.Info("Fetching remote config from %s", remoteConfigURL)
wgconfig, err = loadRemoteConfig(remoteConfigURL, key, reachableAt)
if err != nil {
logger.Error("Failed to load configuration: %v", err)
time.Sleep(5 * time.Second)
continue
}
wgconfig.PrivateKey = key.String()
}
wgconfig.PrivateKey = key.String()
}
wgClient, err = wgctrl.New()
@@ -168,8 +216,8 @@ func main() {
// Ensure the WireGuard peers exist
ensureWireguardPeers(wgconfig.Peers)
if *reportBandwidthTo != "" {
go periodicBandwidthCheck(*reportBandwidthTo)
if reportBandwidthTo != "" {
go periodicBandwidthCheck(reportBandwidthTo)
}
http.HandleFunc("/peer", handlePeer)
@@ -288,10 +336,19 @@ func ensureWireguardInterface(wgconfig WgConfig) error {
if err != nil {
return fmt.Errorf("failed to get interface: %v", err)
}
if err := netlink.LinkSetMTU(link, mtuInt); err != nil {
return fmt.Errorf("failed to set MTU: %v", err)
}
if err := netlink.LinkSetUp(link); err != nil {
return fmt.Errorf("failed to bring up interface: %v", err)
}
if err := ensureMSSClamping(); err != nil {
logger.Warn("Failed to ensure MSS clamping: %v", err)
}
logger.Info("WireGuard interface %s created and configured", interfaceName)
return nil
@@ -369,6 +426,94 @@ func ensureWireguardPeers(peers []Peer) error {
return nil
}
func ensureMSSClamping() error {
// Calculate MSS value (MTU - 40 for IPv4 header (20) and TCP header (20))
mssValue := mtuInt - 40
// Rules to be managed - just the chains, we'll construct the full command separately
chains := []string{"INPUT", "OUTPUT", "FORWARD"}
// First, try to delete any existing rules
for _, chain := range chains {
deleteCmd := exec.Command("/usr/sbin/iptables",
"-t", "mangle",
"-D", chain,
"-p", "tcp",
"--tcp-flags", "SYN,RST", "SYN",
"-j", "TCPMSS",
"--set-mss", fmt.Sprintf("%d", mssValue))
logger.Info("Attempting to delete existing MSS clamping rule for chain %s", chain)
// Try deletion multiple times to handle multiple existing rules
for i := 0; i < 3; i++ {
out, err := deleteCmd.CombinedOutput()
if err != nil {
// Convert exit status 1 to string for better logging
if exitErr, ok := err.(*exec.ExitError); ok {
logger.Debug("Deletion stopped for chain %s: %v (output: %s)",
chain, exitErr.String(), string(out))
}
break // No more rules to delete
}
logger.Info("Deleted MSS clamping rule for chain %s (attempt %d)", chain, i+1)
}
}
// Then add the new rules
var errors []error
for _, chain := range chains {
addCmd := exec.Command("/usr/sbin/iptables",
"-t", "mangle",
"-A", chain,
"-p", "tcp",
"--tcp-flags", "SYN,RST", "SYN",
"-j", "TCPMSS",
"--set-mss", fmt.Sprintf("%d", mssValue))
logger.Info("Adding MSS clamping rule for chain %s", chain)
if out, err := addCmd.CombinedOutput(); err != nil {
errMsg := fmt.Sprintf("Failed to add MSS clamping rule for chain %s: %v (output: %s)",
chain, err, string(out))
logger.Error(errMsg)
errors = append(errors, fmt.Errorf(errMsg))
continue
}
// Verify the rule was added
checkCmd := exec.Command("/usr/sbin/iptables",
"-t", "mangle",
"-C", chain,
"-p", "tcp",
"--tcp-flags", "SYN,RST", "SYN",
"-j", "TCPMSS",
"--set-mss", fmt.Sprintf("%d", mssValue))
if out, err := checkCmd.CombinedOutput(); err != nil {
errMsg := fmt.Sprintf("Rule verification failed for chain %s: %v (output: %s)",
chain, err, string(out))
logger.Error(errMsg)
errors = append(errors, fmt.Errorf(errMsg))
continue
}
logger.Info("Successfully added and verified MSS clamping rule for chain %s", chain)
}
// If we encountered any errors, return them combined
if len(errors) > 0 {
var errMsgs []string
for _, err := range errors {
errMsgs = append(errMsgs, err.Error())
}
return fmt.Errorf("MSS clamping setup encountered errors:\n%s",
strings.Join(errMsgs, "\n"))
}
return nil
}
func handlePeer(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost: