Skip to content
Merged
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
13 changes: 11 additions & 2 deletions .github/workflows/egress-test.yaml.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
branches: [ main ]
paths:
- 'components/egress/**'
- 'components/internal/**'

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
Expand All @@ -23,14 +24,14 @@ jobs:
go-version: '1.24.0'

- name: Run Build
working-directory: components/egress
run: |
cd components/egress
go vet ./...
go build .

- name: Run tests
working-directory: components/egress
run: |
cd components/egress
go test ./...

smoke:
Expand Down Expand Up @@ -70,3 +71,11 @@ jobs:
./tests/bench-dns-nft.sh
env:
BENCH_SAMPLE_SIZE: "20"

- name: Upload egress logs
if: always()
uses: actions/upload-artifact@v4
with:
name: egress-log-for-bench
path: /tmp/egress-logs/
retention-days: 5
1 change: 1 addition & 0 deletions .github/workflows/execd-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
branches: [ main ]
paths:
- 'components/execd/**'
- 'components/internal/**'

permissions:
contents: read
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ingress-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
branches: [ main ]
paths:
- 'components/ingress/**'
- 'components/internal/**'

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
Expand Down
12 changes: 9 additions & 3 deletions components/egress/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,21 @@

FROM golang:1.24-bookworm AS builder

WORKDIR /app
WORKDIR /workspace

COPY go.mod go.sum ./
# Copy only go mod/sum first for better caching
COPY components/egress/go.mod components/egress/go.sum ./components/egress/
# Bring internal module so replace ../internal works during download/build
COPY components/internal ./components/internal

WORKDIR /workspace/components/egress

# Static-ish build (no cgo) to simplify runtime deps
ENV CGO_ENABLED=0
RUN go mod download

COPY . .
# Copy the rest of the egress sources
COPY components/egress ./
RUN go build -o /out/egress .

FROM debian:bookworm-slim
Expand Down
6 changes: 3 additions & 3 deletions components/egress/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,18 @@
set -ex

TAG=${TAG:-latest}
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || realpath "$(dirname "$0")/../..")
cd "${REPO_ROOT}"

docker buildx rm egress-builder || true

docker buildx create --use --name egress-builder

docker buildx inspect --bootstrap

docker buildx ls

docker buildx build \
-t opensandbox/egress:${TAG} \
-t sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/egress:${TAG} \
-f components/egress/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
.
5 changes: 5 additions & 0 deletions components/egress/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@ module github.com/alibaba/opensandbox/egress
go 1.24.0

require (
github.com/alibaba/opensandbox/internal v0.0.0
github.com/miekg/dns v1.1.61
golang.org/x/sys v0.31.0
)

require (
go.uber.org/multierr v1.10.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/tools v0.22.0 // indirect
)

replace github.com/alibaba/opensandbox/internal => ../internal
14 changes: 14 additions & 0 deletions components/egress/go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
Expand All @@ -10,3 +22,5 @@ golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
23 changes: 17 additions & 6 deletions components/egress/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ package main

import (
"context"
"log"
"os"
"os/signal"
"strings"
Expand All @@ -25,12 +24,17 @@ import (
"github.com/alibaba/opensandbox/egress/pkg/constants"
"github.com/alibaba/opensandbox/egress/pkg/dnsproxy"
"github.com/alibaba/opensandbox/egress/pkg/iptables"
"github.com/alibaba/opensandbox/egress/pkg/log"
slogger "github.com/alibaba/opensandbox/internal/logger"
)

func main() {
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()

ctx = withLogger(ctx)
defer log.Logger.Sync()

initialRules, err := dnsproxy.LoadPolicyFromEnvVar(constants.EnvEgressRules)
if err != nil {
log.Fatalf("failed to parse %s: %v", constants.EnvEgressRules, err)
Expand All @@ -39,6 +43,7 @@ func main() {
allowIPs := AllowIPsForNft("/etc/resolv.conf")

mode := parseMode()
log.Infof("enforcement mode: %s", mode)
nftMgr := createNftManager(mode)
proxy, err := dnsproxy.New(initialRules, "")
if err != nil {
Expand All @@ -47,12 +52,12 @@ func main() {
if err := proxy.Start(ctx); err != nil {
log.Fatalf("failed to start dns proxy: %v", err)
}
log.Println("dns proxy started on 127.0.0.1:15353")
log.Infof("dns proxy started on 127.0.0.1:15353")

if err := iptables.SetupRedirect(15353); err != nil {
log.Fatalf("failed to install iptables redirect: %v", err)
}
log.Printf("iptables redirect configured (OUTPUT 53 -> 15353) with SO_MARK bypass for proxy upstream traffic")
log.Infof("iptables redirect configured (OUTPUT 53 -> 15353) with SO_MARK bypass for proxy upstream traffic")

setupNft(ctx, nftMgr, initialRules, proxy, allowIPs)

Expand All @@ -61,13 +66,19 @@ func main() {
if err = startPolicyServer(ctx, proxy, nftMgr, mode, httpAddr, os.Getenv(constants.EnvEgressToken), allowIPs); err != nil {
log.Fatalf("failed to start policy server: %v", err)
}
log.Printf("policy server listening on %s (POST /policy)", httpAddr)
log.Infof("policy server listening on %s (POST /policy)", httpAddr)

<-ctx.Done()
log.Println("received shutdown signal; exiting")
log.Infof("received shutdown signal; exiting")
_ = os.Stderr.Sync()
}

func withLogger(ctx context.Context) context.Context {
level := envOrDefault(constants.EnvEgressLogLevel, "info")
logger := slogger.MustNew(slogger.Config{Level: level}).Named("opensandbox.egress")
return log.WithLogger(ctx, logger)
}

func envOrDefault(key, defaultVal string) string {
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
return v
Expand All @@ -92,7 +103,7 @@ func parseMode() string {
case constants.PolicyDnsNft:
return constants.PolicyDnsNft
default:
log.Printf("invalid %s=%s, falling back to dns", constants.EnvEgressMode, mode)
log.Warnf("invalid %s=%s, falling back to dns", constants.EnvEgressMode, mode)
return constants.PolicyDnsOnly
}
}
6 changes: 3 additions & 3 deletions components/egress/nameserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@
package main

import (
"log"
"net/netip"
"os"
"strconv"

"github.com/alibaba/opensandbox/egress/pkg/constants"
"github.com/alibaba/opensandbox/egress/pkg/dnsproxy"
"github.com/alibaba/opensandbox/egress/pkg/log"
)

// AllowIPsForNft returns the list of IPs to merge into the nft allow set for DNS in dns+nft mode:
Expand Down Expand Up @@ -49,9 +49,9 @@ func AllowIPsForNft(resolvPath string) []netip.Addr {
out = append(out, validated...)

if len(out) > 1 {
log.Printf("[dns] whitelisting proxy listen + %d nameserver(s) for nft: %v", len(validated), formatIPs(out))
log.Infof("[dns] whitelisting proxy listen + %d nameserver(s) for nft: %v", len(validated), formatIPs(out))
} else {
log.Printf("[dns] whitelisting proxy listen (127.0.0.1); no valid nameserver IPs from %s", resolvPath)
log.Infof("[dns] whitelisting proxy listen (127.0.0.1); no valid nameserver IPs from %s", resolvPath)
}
return out
}
Expand Down
10 changes: 6 additions & 4 deletions components/egress/nft.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ package main

import (
"context"
"log"
"net/netip"
"os"
"strings"

"github.com/alibaba/opensandbox/egress/pkg/constants"
"github.com/alibaba/opensandbox/egress/pkg/dnsproxy"
"github.com/alibaba/opensandbox/egress/pkg/log"
"github.com/alibaba/opensandbox/egress/pkg/nftables"
"github.com/alibaba/opensandbox/egress/pkg/policy"
)
Expand All @@ -39,16 +39,18 @@ func createNftManager(mode string) nftApplier {
// nameserverIPs are merged into the allow set at startup so system DNS works (client + proxy upstream, e.g. private DNS).
func setupNft(ctx context.Context, nftMgr nftApplier, initialPolicy *policy.NetworkPolicy, proxy *dnsproxy.Proxy, nameserverIPs []netip.Addr) {
if nftMgr == nil {
log.Warnf("nftables disabled (dns-only mode)")
return
}
log.Infof("applying nftables static policy (dns+nft mode) with %d nameserver IP(s) merged into allow set", len(nameserverIPs))
policyWithNS := initialPolicy.WithExtraAllowIPs(nameserverIPs)
if err := nftMgr.ApplyStatic(ctx, policyWithNS); err != nil {
log.Fatalf("nftables static apply failed: %v", err)
}
log.Printf("nftables static policy applied (table inet opensandbox)")
log.Infof("nftables static policy applied (table inet opensandbox); DNS-resolved IPs will be added to dynamic allow sets")
proxy.SetOnResolved(func(domain string, ips []nftables.ResolvedIP) {
if err := nftMgr.AddResolvedIPs(ctx, ips); err != nil {
log.Printf("[dns] add resolved IPs to nft failed: %v", err)
log.Warnf("[dns] add resolved IPs to nft failed for domain %q: %v", domain, err)
}
})
}
Expand Down Expand Up @@ -81,7 +83,7 @@ func parseNftOptions() nftables.Options {
}
continue
}
log.Printf("ignoring invalid DoH blocklist entry: %s", target)
log.Warnf("ignoring invalid DoH blocklist entry: %s", target)
}
}
return opts
Expand Down
1 change: 1 addition & 0 deletions components/egress/pkg/constants/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const (
EnvEgressHTTPAddr = "OPENSANDBOX_EGRESS_HTTP_ADDR"
EnvEgressToken = "OPENSANDBOX_EGRESS_TOKEN"
EnvEgressRules = "OPENSANDBOX_EGRESS_RULES"
EnvEgressLogLevel = "OPENSANDBOX_EGRESS_LOG_LEVEL"
EnvMaxNameservers = "OPENSANDBOX_EGRESS_MAX_NS"
)

Expand Down
6 changes: 3 additions & 3 deletions components/egress/pkg/dnsproxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ package dnsproxy
import (
"context"
"fmt"
"log"
"net"
"net/netip"
"os"
Expand All @@ -26,6 +25,7 @@ import (

"github.com/miekg/dns"

"github.com/alibaba/opensandbox/egress/pkg/log"
"github.com/alibaba/opensandbox/egress/pkg/nftables"
"github.com/alibaba/opensandbox/egress/pkg/policy"
)
Expand Down Expand Up @@ -117,7 +117,7 @@ func (p *Proxy) serveDNS(w dns.ResponseWriter, r *dns.Msg) {

resp, err := p.forward(r)
if err != nil {
log.Printf("[dns] forward error for %s: %v", domain, err)
log.Warnf("[dns] forward error for %s: %v", domain, err)
fail := new(dns.Msg)
fail.SetRcode(r, dns.RcodeServerFailure)
_ = w.WriteMsg(fail)
Expand Down Expand Up @@ -220,7 +220,7 @@ func discoverUpstream() (string, error) {
cfg, err := dns.ClientConfigFromFile("/etc/resolv.conf")
if err != nil || len(cfg.Servers) == 0 {
if err != nil {
log.Printf("[dns] fallback upstream resolver due to error: %v", err)
log.Warnf("[dns] fallback upstream resolver due to error: %v", err)
}
return fallbackUpstream, nil
}
Expand Down
3 changes: 3 additions & 0 deletions components/egress/pkg/iptables/redirect.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ import (
"strconv"

"github.com/alibaba/opensandbox/egress/pkg/constants"
"github.com/alibaba/opensandbox/egress/pkg/log"
)

// SetupRedirect installs OUTPUT nat redirect for DNS (udp/tcp 53 -> port).
// Packets carrying mark bypassMark will RETURN (used by the proxy's own upstream
// queries to avoid redirect loops). Requires CAP_NET_ADMIN inside the namespace.
func SetupRedirect(port int) error {
log.Infof("installing iptables DNS redirect: OUTPUT port 53 -> %d (mark %s bypass)", port, constants.MarkHex)
targetPort := strconv.Itoa(port)

rules := [][]string{
Expand All @@ -47,5 +49,6 @@ func SetupRedirect(port int) error {
return fmt.Errorf("iptables command failed: %v (output: %s)", err, output)
}
}
log.Infof("iptables DNS redirect installed successfully")
return nil
}
Loading
Loading