Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 56 additions & 73 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,91 +1,71 @@
name: CI

on:
release:
types:
- published
push:
branches:
- master
paths:
- .github/workflows/ci.yml
- cmd/**
- internal/**
- pkg/**
- .dockerignore
- .golangci.yml
- Dockerfile
- go.mod
- go.sum
- main
- develop
- release/*
- beta/*
tags:
- v*
pull_request:
paths:
- .github/workflows/ci.yml
- cmd/**
- internal/**
- pkg/**
- .dockerignore
- .golangci.yml
- Dockerfile
- go.mod
- go.sum
branches:
- main
- develop
- release/*
- beta/*
release:
types: [published]
workflow_dispatch:

jobs:
verify:
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
env:
DOCKER_BUILDKIT: "1"
steps:
- uses: actions/checkout@v4

- uses: reviewdog/action-misspell@v1
- uses: actions/setup-go@v5
with:
locale: "US"
level: error
exclude: |
./internal/storage/servers.json
*.md

- name: Linting
run: docker build --target lint .

- name: Mocks check
run: docker build --target mocks .

- name: Build test image
run: docker build --target test -t test-container .

- name: Run tests in test container
run: |
touch coverage.txt
docker run --rm --device /dev/net/tun \
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
test-container

- name: Build final image
run: docker build -t final-image .
go-version-file: go.mod
cache: true
- name: Verify
run: make verify

codeql:
name: CodeQL
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write

strategy:
fail-fast: false
matrix:
language: ["go"]

steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "^1.23"
- uses: github/codeql-action/init@v3
- name: Checkout repository
uses: actions/checkout@v4

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: go
- uses: github/codeql-action/autobuild@v3
- uses: github/codeql-action/analyze@v3
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and queries in the config file.
# For more information on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-qlpacks

- name: Autobuild
uses: github/codeql-action/autobuild@v3

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3

publish:
if: |
github.repository == 'qdm12/gluetun' &&
(
github.event_name == 'push' ||
github.event_name == 'release' ||
Expand All @@ -109,9 +89,8 @@ jobs:
flavor: |
latest=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
images: |
ghcr.io/qdm12/gluetun
qmcgaw/gluetun
qmcgaw/private-internet-access
ghcr.io/${{ github.repository_owner }}/gluetun
${{ github.repository_owner }}/gluetun
tags: |
type=ref,event=pr
type=semver,pattern=v{{major}}.{{minor}}.{{patch}}
Expand All @@ -122,20 +101,24 @@ jobs:
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3

# Login to Docker Hub (optional, only if you want to push to Docker Hub)
- uses: docker/login-action@v3
if: github.event_name != 'pull_request'
with:
username: qmcgaw
password: ${{ secrets.DOCKERHUB_PASSWORD }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

# Login to GitHub Container Registry
- uses: docker/login-action@v3
if: github.event_name != 'pull_request'
with:
registry: ghcr.io
username: qdm12
password: ${{ github.token }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Short commit
id: shortcommit
run: echo "::set-output name=value::$(git rev-parse --short HEAD)"
run: echo "value=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT

- name: Build and push final image
uses: docker/build-push-action@v6
Expand All @@ -147,4 +130,4 @@ jobs:
COMMIT=${{ steps.shortcommit.outputs.value }}
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
tags: ${{ steps.meta.outputs.tags }}
push: true
push: ${{ github.event_name != 'pull_request' }}
37 changes: 37 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Release
on:
push:
tags:
- 'v*'

jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Generate changelog
id: changelog
run: |
# Simple changelog generation
echo "## Changes" > CHANGELOG.md
git log --pretty=format:"- %s" $(git describe --tags --abbrev=0 HEAD^)..HEAD >> CHANGELOG.md
echo "changelog<<EOF" >> $GITHUB_OUTPUT
cat CHANGELOG.md >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT

- name: Create Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
body: ${{ steps.changelog.outputs.changelog }}
draft: false
prerelease: false
71 changes: 41 additions & 30 deletions internal/firewall/enable.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,36 +41,6 @@ func (c *Config) SetEnabled(ctx context.Context, enabled bool) (err error) {
return nil
}

func (c *Config) disable(ctx context.Context) (err error) {
if err = c.clearAllRules(ctx); err != nil {
return fmt.Errorf("clearing all rules: %w", err)
}
if err = c.setIPv4AllPolicies(ctx, "ACCEPT"); err != nil {
return fmt.Errorf("setting ipv4 policies: %w", err)
}
if err = c.setIPv6AllPolicies(ctx, "ACCEPT"); err != nil {
return fmt.Errorf("setting ipv6 policies: %w", err)
}

const remove = true
err = c.redirectPorts(ctx, remove)
if err != nil {
return fmt.Errorf("removing port redirections: %w", err)
}

return nil
}

// To use in defered call when enabling the firewall.
func (c *Config) fallbackToDisabled(ctx context.Context) {
if ctx.Err() != nil {
return
}
if err := c.disable(ctx); err != nil {
c.logger.Error("failed reversing firewall changes: " + err.Error())
}
}

func (c *Config) enable(ctx context.Context) (err error) {
touched := false
if err = c.setIPv4AllPolicies(ctx, "DROP"); err != nil {
Expand All @@ -90,6 +60,11 @@ func (c *Config) enable(ctx context.Context) (err error) {
}
}()

// Clear any previously applied post-rules
if err = c.clearAppliedPostRules(ctx); err != nil {
c.logger.Warn("failed to clear previous post-rules: " + err.Error())
}

// Loopback traffic
if err = c.acceptInputThroughInterface(ctx, "lo", remove); err != nil {
return err
Expand Down Expand Up @@ -144,13 +119,49 @@ func (c *Config) enable(ctx context.Context) (err error) {
return fmt.Errorf("redirecting ports: %w", err)
}

// Apply post-rules only once at the end
if err := c.runUserPostRules(ctx, c.customRulesPath, remove); err != nil {
return fmt.Errorf("running user defined post firewall rules: %w", err)
}

return nil
}

func (c *Config) disable(ctx context.Context) (err error) {
// Clear applied post-rules when disabling
if err = c.clearAppliedPostRules(ctx); err != nil {
c.logger.Warn("failed to clear post-rules during disable: " + err.Error())
}

if err = c.clearAllRules(ctx); err != nil {
return fmt.Errorf("clearing all rules: %w", err)
}
if err = c.setIPv4AllPolicies(ctx, "ACCEPT"); err != nil {
return fmt.Errorf("setting ipv4 policies: %w", err)
}
if err = c.setIPv6AllPolicies(ctx, "ACCEPT"); err != nil {
return fmt.Errorf("setting ipv6 policies: %w", err)
}

const remove = true
err = c.redirectPorts(ctx, remove)
if err != nil {
return fmt.Errorf("removing port redirections: %w", err)
}

return nil
}

// To use in defered call when enabling the firewall.
func (c *Config) fallbackToDisabled(ctx context.Context) {
if ctx.Err() != nil {
return
}
if err := c.disable(ctx); err != nil {
c.logger.Error("failed reversing firewall changes: " + err.Error())
}
}

func (c *Config) allowVPNIP(ctx context.Context) (err error) {
if !c.vpnConnection.IP.IsValid() {
return nil
Expand Down
29 changes: 29 additions & 0 deletions internal/firewall/firewall.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package firewall
import (
"context"
"net/netip"
"strings"
"sync"

"github.com/qdm12/gluetun/internal/models"
Expand All @@ -29,6 +30,7 @@ type Config struct { //nolint:maligned
outboundSubnets []netip.Prefix
allowedInputPorts map[uint16]map[string]struct{} // port to interfaces set mapping
portRedirections portRedirections
appliedPostRules []string // Track applied post-rules to avoid duplicates
stateMutex sync.Mutex
}

Expand Down Expand Up @@ -60,3 +62,30 @@ func NewConfig(ctx context.Context, logger Logger,
localNetworks: localNetworks,
}, nil
}

// clearAppliedPostRules removes all previously applied post-rules
func (c *Config) clearAppliedPostRules(ctx context.Context) error {
for _, rule := range c.appliedPostRules {
flippedRule := flipRule(rule)
if strings.Contains(rule, "ip6tables") {
if err := c.runIP6tablesInstruction(ctx, flippedRule); err != nil {
c.logger.Debug("failed to remove post-rule (may not exist): " + err.Error())
}
} else {
if err := c.runIptablesInstruction(ctx, flippedRule); err != nil {
c.logger.Debug("failed to remove post-rule (may not exist): " + err.Error())
}
}
}
c.appliedPostRules = nil
return nil
}

// applyPostRulesOnce applies post-rules only if they haven't been applied yet
func (c *Config) applyPostRulesOnce(ctx context.Context) error {
if len(c.appliedPostRules) > 0 {
c.logger.Debug("post-rules already applied, skipping")
return nil
}
return c.runUserPostRules(ctx, c.customRulesPath, false)
}
Loading