Files
wehub-resource-sync e0e362d700
SDK Tests / changes (push) Successful in 2m29s
Real E2E Tests / changes (push) Successful in 2m29s
Deploy Docs Pages / build (push) Has been cancelled
Deploy Docs Pages / deploy (push) Has been cancelled
Real E2E Tests / JavaScript E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Python E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Java E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / C# E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Go E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Real E2E CI (push) Has been cancelled
SDK Tests / SDK CI (push) Has been cancelled
SDK Tests / CLI Tests (push) Has been cancelled
SDK Tests / Python SDK Quality (code-interpreter) (push) Has been cancelled
SDK Tests / Python SDK Quality (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (sandbox) (push) Has been cancelled
SDK Tests / CLI Quality (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Go SDK Quality And Tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:39:33 +08:00

270 lines
8.8 KiB
Go

// Copyright 2026 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package nftables
import (
"context"
"fmt"
"os/exec"
"strings"
"sync"
"github.com/alibaba/opensandbox/egress/pkg/constants"
"github.com/alibaba/opensandbox/egress/pkg/log"
"github.com/alibaba/opensandbox/egress/pkg/policy"
"github.com/alibaba/opensandbox/egress/pkg/telemetry"
)
const (
tableName = "opensandbox"
chainName = "egress"
allowV4Set = "allow_v4"
allowV6Set = "allow_v6"
denyV4Set = "deny_v4"
denyV6Set = "deny_v6"
dohBlockV4Set = "doh_block_v4"
dohBlockV6Set = "doh_block_v6"
)
type runner func(ctx context.Context, script string) ([]byte, error)
type Options struct {
BlockDoT bool
BlockDoH443 bool
DoHBlocklistV4 []string
DoHBlocklistV6 []string
}
type Manager struct {
run runner
opts Options
mu sync.Mutex
}
func NewManager() *Manager {
return &Manager{run: defaultRunner, opts: Options{BlockDoT: true}}
}
func NewManagerWithRunner(r runner) *Manager {
return &Manager{run: r, opts: Options{BlockDoT: true}}
}
func NewManagerWithRunnerAndOptions(r runner, opts Options) *Manager {
return &Manager{run: r, opts: opts}
}
func NewManagerWithOptions(opts Options) *Manager {
return &Manager{run: defaultRunner, opts: opts}
}
func (m *Manager) ApplyStatic(ctx context.Context, p *policy.NetworkPolicy) error {
if p == nil {
p = policy.DefaultDenyPolicy()
}
allowV4, allowV6, denyV4, denyV6 := p.StaticIPSets()
log.Infof("nftables: applying static policy: default=%s, allow_v4=%d, allow_v6=%d, deny_v4=%d, deny_v6=%d",
p.DefaultAction, len(allowV4), len(allowV6), len(denyV4), len(denyV6))
m.mu.Lock()
defer m.mu.Unlock()
script, err := buildRuleset(p, m.opts)
if err != nil {
return err
}
if _, err := m.run(ctx, script); err != nil {
if isMissingTableError(err) {
fallback := removeDeleteTableLine(script)
if fallback != script {
if _, retryErr := m.run(ctx, fallback); retryErr == nil {
telemetry.SetNftablesRuleCount(telemetry.NftRuleCountFromPolicy(p))
telemetry.RecordNftablesUpdate()
return nil
}
}
}
return err
}
telemetry.SetNftablesRuleCount(telemetry.NftRuleCountFromPolicy(p))
telemetry.RecordNftablesUpdate()
log.Infof("nftables: static policy applied successfully")
return nil
}
func (m *Manager) AddResolvedIPs(ctx context.Context, ips []ResolvedIP) error {
if len(ips) == 0 {
return nil
}
m.mu.Lock()
defer m.mu.Unlock()
script := buildAddResolvedIPsScript(tableName, ips)
if script == "" {
return nil
}
log.Debugf("nftables: adding %d resolved IP(s) to dynamic allow sets with script statement %s", len(ips), script)
_, err := m.run(ctx, script)
if err == nil {
telemetry.RecordNftablesUpdate()
}
return err
}
// RemoveEnforcement drops inet opensandbox; missing table is not an error.
func (m *Manager) RemoveEnforcement(ctx context.Context) error {
m.mu.Lock()
defer m.mu.Unlock()
script := fmt.Sprintf("delete table inet %s\n", tableName)
_, err := m.run(ctx, script)
if err != nil {
msg := strings.ToLower(err.Error())
if strings.Contains(msg, "no such file") || strings.Contains(msg, "does not exist") {
return nil
}
return err
}
log.Infof("nftables: removed table inet %s", tableName)
return nil
}
func buildRuleset(p *policy.NetworkPolicy, opts Options) (string, error) {
allowV4, allowV6, denyV4, denyV6 := p.StaticIPSets()
var err error
if allowV4, err = normalizeNFTIntervalSet(allowV4); err != nil {
return "", err
}
if allowV6, err = normalizeNFTIntervalSet(allowV6); err != nil {
return "", err
}
if denyV4, err = normalizeNFTIntervalSet(denyV4); err != nil {
return "", err
}
if denyV6, err = normalizeNFTIntervalSet(denyV6); err != nil {
return "", err
}
dohBlockV4 := opts.DoHBlocklistV4
dohBlockV6 := opts.DoHBlocklistV6
if len(dohBlockV4) > 0 {
if dohBlockV4, err = normalizeNFTIntervalSet(dohBlockV4); err != nil {
return "", err
}
}
if len(dohBlockV6) > 0 {
if dohBlockV6, err = normalizeNFTIntervalSet(dohBlockV6); err != nil {
return "", err
}
}
var b strings.Builder
fmt.Fprintf(&b, "delete table inet %s\n", tableName)
fmt.Fprintf(&b, "add table inet %s\n", tableName)
fmt.Fprintf(&b, "add set inet %s %s { type ipv4_addr; flags interval; }\n", tableName, allowV4Set)
fmt.Fprintf(&b, "add set inet %s %s { type ipv4_addr; flags interval; }\n", tableName, denyV4Set)
fmt.Fprintf(&b, "add set inet %s %s { type ipv6_addr; flags interval; }\n", tableName, allowV6Set)
fmt.Fprintf(&b, "add set inet %s %s { type ipv6_addr; flags interval; }\n", tableName, denyV6Set)
fmt.Fprintf(&b, "add set inet %s %s { type ipv4_addr; timeout %ds; }\n", tableName, dynAllowV4Set, dynSetTimeoutS)
fmt.Fprintf(&b, "add set inet %s %s { type ipv6_addr; timeout %ds; }\n", tableName, dynAllowV6Set, dynSetTimeoutS)
if len(dohBlockV4) > 0 {
fmt.Fprintf(&b, "add set inet %s %s { type ipv4_addr; flags interval; }\n", tableName, dohBlockV4Set)
}
if len(dohBlockV6) > 0 {
fmt.Fprintf(&b, "add set inet %s %s { type ipv6_addr; flags interval; }\n", tableName, dohBlockV6Set)
}
writeElements(&b, allowV4Set, allowV4)
writeElements(&b, denyV4Set, denyV4)
writeElements(&b, allowV6Set, allowV6)
writeElements(&b, denyV6Set, denyV6)
writeElements(&b, dohBlockV4Set, dohBlockV4)
writeElements(&b, dohBlockV6Set, dohBlockV6)
chainPolicy := "drop"
if p.DefaultAction == policy.ActionAllow {
chainPolicy = "accept"
}
fmt.Fprintf(&b, "add chain inet %s %s { type filter hook output priority 0; policy %s; }\n", tableName, chainName, chainPolicy)
fmt.Fprintf(&b, "add rule inet %s %s ct state established,related accept\n", tableName, chainName)
fmt.Fprintf(&b, "add rule inet %s %s meta mark %s accept\n", tableName, chainName, constants.MarkHex)
fmt.Fprintf(&b, "add rule inet %s %s oifname \"lo\" accept\n", tableName, chainName)
if opts.BlockDoT {
fmt.Fprintf(&b, "add rule inet %s %s tcp dport 853 drop\n", tableName, chainName)
fmt.Fprintf(&b, "add rule inet %s %s udp dport 853 drop\n", tableName, chainName)
}
if opts.BlockDoH443 {
if len(dohBlockV4) == 0 && len(dohBlockV6) == 0 {
// strict: drop all 443 when enabled but no blocklist provided
fmt.Fprintf(&b, "add rule inet %s %s tcp dport 443 drop\n", tableName, chainName)
} else {
if len(dohBlockV4) > 0 {
fmt.Fprintf(&b, "add rule inet %s %s ip daddr @%s tcp dport 443 drop\n", tableName, chainName, dohBlockV4Set)
}
if len(dohBlockV6) > 0 {
fmt.Fprintf(&b, "add rule inet %s %s ip6 daddr @%s tcp dport 443 drop\n", tableName, chainName, dohBlockV6Set)
}
}
}
fmt.Fprintf(&b, "add rule inet %s %s ip daddr @%s drop\n", tableName, chainName, denyV4Set)
fmt.Fprintf(&b, "add rule inet %s %s ip6 daddr @%s drop\n", tableName, chainName, denyV6Set)
fmt.Fprintf(&b, "add rule inet %s %s ip daddr @%s accept\n", tableName, chainName, dynAllowV4Set)
fmt.Fprintf(&b, "add rule inet %s %s ip6 daddr @%s accept\n", tableName, chainName, dynAllowV6Set)
fmt.Fprintf(&b, "add rule inet %s %s ip daddr @%s accept\n", tableName, chainName, allowV4Set)
fmt.Fprintf(&b, "add rule inet %s %s ip6 daddr @%s accept\n", tableName, chainName, allowV6Set)
if chainPolicy == "drop" {
fmt.Fprintf(&b, "add rule inet %s %s drop\n", tableName, chainName)
}
return b.String(), nil
}
func writeElements(b *strings.Builder, setName string, elems []string) {
if len(elems) == 0 {
return
}
fmt.Fprintf(b, "add element inet %s %s { %s }\n", tableName, setName, strings.Join(elems, ", "))
}
func defaultRunner(ctx context.Context, script string) ([]byte, error) {
cmd := exec.CommandContext(ctx, "nft", "-f", "-")
cmd.Stdin = strings.NewReader(script)
output, err := cmd.CombinedOutput()
if err != nil {
return output, fmt.Errorf("nft apply failed: %w (output: %s)", err, strings.TrimSpace(string(output)))
}
return output, nil
}
func isMissingTableError(err error) bool {
if err == nil {
return false
}
msg := strings.ToLower(err.Error())
return strings.Contains(msg, "no such file or directory") && strings.Contains(msg, "delete table inet "+tableName)
}
func removeDeleteTableLine(script string) string {
lines := strings.Split(script, "\n")
var filtered []string
for _, l := range lines {
if strings.HasPrefix(l, "delete table inet "+tableName) {
continue
}
if strings.TrimSpace(l) == "" {
continue
}
filtered = append(filtered, l)
}
return strings.Join(filtered, "\n")
}