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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.DS_Store
.idea

./boundary # Compiled binary from makefile
# Compiled binary from makefile
/boundary
9 changes: 9 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# Variables
BINARY_NAME := boundary
BUILD_DIR := build
INSTALL_PATH ?= /usr/local/bin
VERSION := $(shell git describe --tags --exact-match 2>/dev/null || echo "dev-$(shell git rev-parse --short HEAD)")
LDFLAGS := -s -w -X main.version=$(VERSION)

Expand All @@ -18,6 +19,13 @@ build:
go build -ldflags="$(LDFLAGS)" -o $(BINARY_NAME) ./cmd/boundary
@echo "✓ Built $(BINARY_NAME)"

# Install binary to $(INSTALL_PATH) (default: /usr/local/bin).
.PHONY: install
install: build
@echo "Installing $(BINARY_NAME) to $(INSTALL_PATH)..."
sudo install -m 755 $(BINARY_NAME) $(INSTALL_PATH)/$(BINARY_NAME)
@echo "✓ Installed $(INSTALL_PATH)/$(BINARY_NAME)"

# Build for all supported platforms
.PHONY: build-all
build-all:
Expand Down Expand Up @@ -150,6 +158,7 @@ help:
@echo "Available targets:"
@echo " build Build for current platform"
@echo " build-all Build for all supported platforms"
@echo " install Install binary to $(INSTALL_PATH) (may need sudo)"
@echo " deps Download and verify dependencies"
@echo " test Run tests"
@echo " test-coverage Run tests with coverage report"
Expand Down
14 changes: 7 additions & 7 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,6 @@ func BaseCommand(version string) *serpent.Command {
Value: &cliConfig.PprofPort,
YAML: "pprof_port",
},
{
Flag: "configure-dns-for-local-stub-resolver",
Env: "BOUNDARY_CONFIGURE_DNS_FOR_LOCAL_STUB_RESOLVER",
Description: "Configure DNS for local stub resolver (e.g., systemd-resolved). Only needed when /etc/resolv.conf contains nameserver 127.0.0.53.",
Value: &cliConfig.ConfigureDNSForLocalStubResolver,
YAML: "configure_dns_for_local_stub_resolver",
},
{
Flag: "jail-type",
Env: "BOUNDARY_JAIL_TYPE",
Expand All @@ -135,6 +128,13 @@ func BaseCommand(version string) *serpent.Command {
Value: &cliConfig.JailType,
YAML: "jail_type",
},
{
Flag: "use-real-dns",
Env: "BOUNDARY_USE_REAL_DNS",
Description: "Use real DNS in the jail instead of the dummy DNS (allows DNS exfiltration). Default: false.",
Value: &cliConfig.UseRealDNS,
YAML: "use_real_dns",
},
{
Flag: "disable-audit-logs",
Env: "DISABLE_AUDIT_LOGS",
Expand Down
6 changes: 3 additions & 3 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ type CliConfig struct {
ProxyPort serpent.Int64 `yaml:"proxy_port"`
PprofEnabled serpent.Bool `yaml:"pprof_enabled"`
PprofPort serpent.Int64 `yaml:"pprof_port"`
ConfigureDNSForLocalStubResolver serpent.Bool `yaml:"configure_dns_for_local_stub_resolver"`
JailType serpent.String `yaml:"jail_type"`
UseRealDNS serpent.Bool `yaml:"use_real_dns"`
DisableAuditLogs serpent.Bool `yaml:"disable_audit_logs"`
LogProxySocketPath serpent.String `yaml:"log_proxy_socket_path"`
}
Expand All @@ -77,8 +77,8 @@ type AppConfig struct {
ProxyPort int64
PprofEnabled bool
PprofPort int64
ConfigureDNSForLocalStubResolver bool
JailType JailType
UseRealDNS bool
TargetCMD []string
UserInfo *UserInfo
DisableAuditLogs bool
Expand Down Expand Up @@ -107,8 +107,8 @@ func NewAppConfigFromCliConfig(cfg CliConfig, targetCMD []string) (AppConfig, er
ProxyPort: cfg.ProxyPort.Value(),
PprofEnabled: cfg.PprofEnabled.Value(),
PprofPort: cfg.PprofPort.Value(),
ConfigureDNSForLocalStubResolver: cfg.ConfigureDNSForLocalStubResolver.Value(),
JailType: jailType,
UseRealDNS: cfg.UseRealDNS.Value(),
TargetCMD: targetCMD,
UserInfo: userInfo,
DisableAuditLogs: cfg.DisableAuditLogs.Value(),
Expand Down
95 changes: 95 additions & 0 deletions dnsdummy/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package dnsdummy

import (
"log/slog"

"github.com/miekg/dns"
)

// DummyA is the IPv4 address returned for every A record query (arbitrary non-loopback).
const DummyA = "6.6.6.6"

// DummyAAAA is the IPv6 address returned for every AAAA record query (documentation prefix).
const DummyAAAA = "2001:db8::1"

// Server is a minimal DNS server that responds to every query with a dummy A record.
// Used inside the network namespace to prevent DNS exfiltration.
type Server struct {
udp *dns.Server
tcp *dns.Server
logger *slog.Logger
}

// NewServer creates a dummy DNS server that listens on addr (e.g. "127.0.0.1:53").
func NewServer(addr string, logger *slog.Logger) *Server {
handler := dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
m := new(dns.Msg)
m.SetReply(r)
m.Authoritative = true

for _, q := range r.Question {
switch q.Qtype {
case dns.TypeA:
rr, err := dns.NewRR(q.Name + " 1 IN A " + DummyA)
if err != nil {
continue
}
m.Answer = append(m.Answer, rr)
case dns.TypeAAAA:
rr, err := dns.NewRR(q.Name + " 1 IN AAAA " + DummyAAAA)
if err != nil {
continue
}
m.Answer = append(m.Answer, rr)
default:
m.Rcode = dns.RcodeSuccess
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this default case is redundant because m.SetReply() does exactly this internally

}
}

if err := w.WriteMsg(m); err != nil {
logger.Debug("dummy DNS: failed to write response", "error", err)
}
})

udp := &dns.Server{
Addr: addr,
Net: "udp",
Handler: handler,
}
tcp := &dns.Server{
Addr: addr,
Net: "tcp",
Handler: handler,
}

return &Server{udp: udp, tcp: tcp, logger: logger}
}

// ListenAndServe starts the UDP and TCP servers in goroutines and returns immediately.
// Logger must be non-nil; server errors are logged via s.logger.
func (s *Server) ListenAndServe() {
go func() {
if err := s.tcp.ListenAndServe(); err != nil {
s.logger.Error("dummy DNS TCP server failed", "error", err)
}
}()
go func() {
if err := s.udp.ListenAndServe(); err != nil {
s.logger.Error("dummy DNS UDP server failed", "error", err)
}
}()
}

// Shutdown stops the servers.
func (s *Server) Shutdown() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean to call this somewhere?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be nice to call it in boundary-child process, after target process is finished, but it requires some refactoring.

if err := s.udp.Shutdown(); err != nil {
s.logger.Error("dummy DNS UDP server shutdown failed", "error", err)
}
if err := s.tcp.Shutdown(); err != nil {
s.logger.Error("dummy DNS TCP server shutdown failed", "error", err)
}
}

// DefaultDummyDNSPort is the port the dummy DNS server listens on (high port to avoid CAP_NET_BIND_SERVICE).
// Traffic to port 53 is DNAT'd to this port in the namespace.
const DefaultDummyDNSPort = "5353"
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ require (
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.72 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/pion/transport/v2 v2.2.10 // indirect
Expand All @@ -34,7 +35,11 @@ require (
go.opentelemetry.io/otel/trace v1.38.0 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/term v0.38.0 // indirect
golang.org/x/tools v0.40.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
kernel.org/pub/linux/libs/security/libcap/psx v1.2.77 // indirect
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
Expand Down Expand Up @@ -150,6 +152,8 @@ golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
Expand All @@ -164,6 +168,8 @@ golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwE
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand Down Expand Up @@ -198,6 +204,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
Expand Down
11 changes: 7 additions & 4 deletions nsjail_manager/child.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,15 @@ func RunChild(logger *slog.Logger, targetCMD []string) error {
}
logger.Info("child networking is successfully configured")

if os.Getenv("CONFIGURE_DNS_FOR_LOCAL_STUB_RESOLVER") == "true" {
err = nsjail.ConfigureDNSForLocalStubResolver()
if os.Getenv("USE_REAL_DNS") == "true" {
logger.Info("using real DNS in namespace (--use-real-dns)")
} else {
// Run dummy DNS server in namespace and redirect all DNS to it to prevent DNS exfiltration
err = nsjail.StartDummyDNSAndRedirect(logger)
if err != nil {
return fmt.Errorf("failed to configure DNS in namespace: %v", err)
return fmt.Errorf("failed to start dummy DNS in namespace: %v", err)
}
logger.Info("DNS in namespace is configured successfully")
logger.Info("dummy DNS server started in namespace")
}

// Program to run
Expand Down
9 changes: 8 additions & 1 deletion nsjail_manager/nsjail/command_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type command struct {
ambientCaps []uintptr

// If ignoreErr isn't empty and this specific error occurs, suppress it (don’t log it, don’t return it).
// If ignoreErr is "*" - ignore any error;
ignoreErr string
}

Expand All @@ -40,7 +41,13 @@ func newCommandWithIgnoreErr(
}

func (cmd *command) isIgnorableError(err string) bool {
return cmd.ignoreErr != "" && strings.Contains(err, cmd.ignoreErr)
if cmd.ignoreErr == "" {
return false
}
if cmd.ignoreErr == "*" {
return true
}
return strings.Contains(err, cmd.ignoreErr)
}

type commandRunner struct {
Expand Down
48 changes: 48 additions & 0 deletions nsjail_manager/nsjail/dummy_dns.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package nsjail

import (
"log/slog"
"os/exec"

"github.com/coder/boundary/dnsdummy"
"golang.org/x/sys/unix"
)

// StartDummyDNSAndRedirect starts a dummy DNS server in-process (goroutine) listening on
// 127.0.0.1:5353 and redirects all DNS traffic (UDP/TCP port 53) in the namespace to it
// via iptables. This prevents DNS exfiltration: all DNS queries get a dummy response (6.6.6.6).
// Must be called from inside the network namespace.
func StartDummyDNSAndRedirect(logger *slog.Logger) error {
addr := "127.0.0.1:" + dnsdummy.DefaultDummyDNSPort
server := dnsdummy.NewServer(addr, logger)
server.ListenAndServe()
logger.Debug("dummy DNS server started in-process", "addr", addr)

// Redirect all DNS (UDP and TCP port 53) to 127.0.0.1:5353
runner := newCommandRunner([]*command{
// Allow loopback-destined traffic to pass through NAT so DNAT to 127.0.0.1 works.
// Best-effort: in some environments (e.g. Sysbox/Docker) this command may not work,
// but DNS setup should work anyway.
newCommandWithIgnoreErr(
"Allow loopback-destined traffic for dummy DNS (route_localnet)",
exec.Command("sysctl", "-w", "net.ipv4.conf.all.route_localnet=1"),
[]uintptr{uintptr(unix.CAP_NET_ADMIN)},
"*",
),
newCommand(
"Redirect UDP DNS to dummy server",
exec.Command("iptables", "-t", "nat", "-A", "OUTPUT", "-p", "udp", "--dport", "53", "-j", "DNAT", "--to-destination", addr),
[]uintptr{uintptr(unix.CAP_NET_ADMIN)},
),
newCommand(
"Redirect TCP DNS to dummy server",
exec.Command("iptables", "-t", "nat", "-A", "OUTPUT", "-p", "tcp", "--dport", "53", "-j", "DNAT", "--to-destination", addr),
[]uintptr{uintptr(unix.CAP_NET_ADMIN)},
),
})
if err := runner.run(); err != nil {
return err
}

return nil
}
34 changes: 17 additions & 17 deletions nsjail_manager/nsjail/jail.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,32 +18,32 @@ type Jailer interface {
}

type Config struct {
Logger *slog.Logger
HttpProxyPort int
HomeDir string
ConfigDir string
CACertPath string
ConfigureDNSForLocalStubResolver bool
Logger *slog.Logger
HttpProxyPort int
HomeDir string
ConfigDir string
CACertPath string
UseRealDNS bool
}

// LinuxJail implements Jailer using Linux network namespaces
type LinuxJail struct {
logger *slog.Logger
vethHostName string // Host-side veth interface name for iptables rules
vethJailName string // Jail-side veth interface name for iptables rules
httpProxyPort int
configDir string
caCertPath string
configureDNSForLocalStubResolver bool
httpProxyPort int
configDir string
caCertPath string
useRealDNS bool
}

func NewLinuxJail(config Config) (*LinuxJail, error) {
return &LinuxJail{
logger: config.Logger,
httpProxyPort: config.HttpProxyPort,
configDir: config.ConfigDir,
caCertPath: config.CACertPath,
configureDNSForLocalStubResolver: config.ConfigureDNSForLocalStubResolver,
logger: config.Logger,
httpProxyPort: config.HttpProxyPort,
configDir: config.ConfigDir,
caCertPath: config.CACertPath,
useRealDNS: config.UseRealDNS,
}, nil
}

Expand Down Expand Up @@ -71,8 +71,8 @@ func (l *LinuxJail) Command(command []string) *exec.Cmd {
cmd.Env = getEnvsForTargetProcess(l.configDir, l.caCertPath)
cmd.Env = append(cmd.Env, "CHILD=true")
cmd.Env = append(cmd.Env, fmt.Sprintf("VETH_JAIL_NAME=%v", l.vethJailName))
if l.configureDNSForLocalStubResolver {
cmd.Env = append(cmd.Env, "CONFIGURE_DNS_FOR_LOCAL_STUB_RESOLVER=true")
if l.useRealDNS {
cmd.Env = append(cmd.Env, "USE_REAL_DNS=true")
}
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
Expand Down
Loading
Loading