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
212 lines
5.7 KiB
Go
212 lines
5.7 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 main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/alibaba/opensandbox/egress/pkg/constants"
|
|
"github.com/alibaba/opensandbox/egress/pkg/log"
|
|
"github.com/alibaba/opensandbox/egress/pkg/policy"
|
|
slogger "github.com/alibaba/opensandbox/internal/logger"
|
|
)
|
|
|
|
const maxPolicyBodyBytes = 1 << 20
|
|
|
|
func readPolicyRequestBody(r *http.Request) (string, error) {
|
|
body, err := io.ReadAll(io.LimitReader(r.Body, maxPolicyBodyBytes))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
raw := strings.TrimSpace(string(body))
|
|
log.Infof("policy API: request body (%s %s): %s", r.Method, r.URL.Path, raw)
|
|
return raw, nil
|
|
}
|
|
|
|
func patchMergedPolicy(base *policy.NetworkPolicy, patchRules []policy.EgressRule) (*policy.NetworkPolicy, error) {
|
|
if base == nil {
|
|
base = policy.DefaultDenyPolicy()
|
|
}
|
|
baseCopy := *base
|
|
baseCopy.Egress = append([]policy.EgressRule(nil), base.Egress...)
|
|
|
|
merged := mergeEgressRules(baseCopy.Egress, patchRules)
|
|
rawMerged, err := json.Marshal(policy.NetworkPolicy{
|
|
DefaultAction: baseCopy.DefaultAction,
|
|
Egress: merged,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return policy.ParsePolicy(string(rawMerged))
|
|
}
|
|
|
|
func mergeEgressRules(base, additions []policy.EgressRule) []policy.EgressRule {
|
|
if len(additions) == 0 {
|
|
return base
|
|
}
|
|
out := make([]policy.EgressRule, 0, len(base)+len(additions))
|
|
seen := make(map[string]struct{})
|
|
|
|
// patch rules win on same target; base fills the rest
|
|
for _, r := range additions {
|
|
key := mergeKey(r)
|
|
if _, ok := seen[key]; ok {
|
|
continue
|
|
}
|
|
seen[key] = struct{}{}
|
|
out = append(out, r)
|
|
}
|
|
for _, r := range base {
|
|
key := mergeKey(r)
|
|
if _, ok := seen[key]; ok {
|
|
continue
|
|
}
|
|
seen[key] = struct{}{}
|
|
out = append(out, r)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// removeRulesByTarget returns a new slice with rules matching targets removed,
|
|
// plus the removed rules. Domain targets are matched case-insensitively.
|
|
// Targets not found are silently ignored.
|
|
func removeRulesByTarget(rules []policy.EgressRule, targets []string) (kept, removed []policy.EgressRule) {
|
|
if len(targets) == 0 || len(rules) == 0 {
|
|
return rules, nil
|
|
}
|
|
removeSet := make(map[string]struct{}, len(targets))
|
|
for _, t := range targets {
|
|
key := strings.ToLower(strings.TrimSpace(t))
|
|
if key == "" {
|
|
continue
|
|
}
|
|
removeSet[key] = struct{}{}
|
|
}
|
|
kept = make([]policy.EgressRule, 0, len(rules))
|
|
for _, r := range rules {
|
|
if _, ok := removeSet[strings.ToLower(r.Target)]; ok {
|
|
removed = append(removed, r)
|
|
} else {
|
|
kept = append(kept, r)
|
|
}
|
|
}
|
|
return kept, removed
|
|
}
|
|
|
|
// mergeKey: domain targets lowercased for dedupe; IP/CIDR left as-is.
|
|
func mergeKey(r policy.EgressRule) string {
|
|
if r.Target == "" {
|
|
return r.Target
|
|
}
|
|
return strings.ToLower(r.Target)
|
|
}
|
|
|
|
func maxEgressRulesFromEnv() int {
|
|
s := strings.TrimSpace(os.Getenv(constants.EnvMaxEgressRules))
|
|
if s == "" {
|
|
return constants.DefaultMaxEgressRules
|
|
}
|
|
n, err := strconv.Atoi(s)
|
|
if err != nil || n < 0 {
|
|
return constants.DefaultMaxEgressRules
|
|
}
|
|
if n == 0 {
|
|
return 0
|
|
}
|
|
return n
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, payload any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(payload)
|
|
}
|
|
|
|
func modeFromPolicy(p *policy.NetworkPolicy) string {
|
|
if p == nil {
|
|
return "deny_all"
|
|
}
|
|
if p.DefaultAction == policy.ActionAllow && len(p.Egress) == 0 {
|
|
return "allow_all"
|
|
} else if p.DefaultAction == policy.ActionDeny && len(p.Egress) == 0 {
|
|
return "deny_all"
|
|
}
|
|
|
|
return "enforcing"
|
|
}
|
|
|
|
func policyRuleSummary(p *policy.NetworkPolicy) []map[string]string {
|
|
if p == nil {
|
|
return nil
|
|
}
|
|
return egressRulesSummary(p.Egress)
|
|
}
|
|
|
|
func egressRulesSummary(egress []policy.EgressRule) []map[string]string {
|
|
out := make([]map[string]string, 0, len(egress))
|
|
for _, r := range egress {
|
|
out = append(out, map[string]string{
|
|
"action": r.Action,
|
|
"target": r.Target,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func logEgressLoaded(pol *policy.NetworkPolicy) {
|
|
if pol == nil {
|
|
pol = policy.DefaultDenyPolicy()
|
|
}
|
|
fields := []slogger.Field{
|
|
{Key: "opensandbox.event", Value: "egress.loaded"},
|
|
{Key: "egress.default", Value: pol.DefaultAction},
|
|
{Key: "rules", Value: policyRuleSummary(pol)},
|
|
}
|
|
log.Logger.With(fields...).Infof("egress policy loaded")
|
|
}
|
|
|
|
// logEgressUpdated: egress.updated event. rules is only the delta for this request (PATCH: patch list;
|
|
// POST/PUT: full body egress; reset: empty), defaultAction is the policy after apply.
|
|
func logEgressUpdated(defaultAction string, deltaEgress []policy.EgressRule) {
|
|
fields := []slogger.Field{
|
|
{Key: "opensandbox.event", Value: "egress.updated"},
|
|
{Key: "egress.default", Value: defaultAction},
|
|
{Key: "rules", Value: egressRulesSummary(deltaEgress)},
|
|
}
|
|
log.Logger.With(fields...).Infof("egress policy updated")
|
|
}
|
|
|
|
func logEgressUpdateFailedWarn(msg string) {
|
|
fields := []slogger.Field{
|
|
{Key: "opensandbox.event", Value: "egress.update_failed"},
|
|
{Key: "error", Value: msg},
|
|
}
|
|
log.Logger.With(fields...).Warnf("egress policy update failed")
|
|
}
|
|
|
|
func logEgressUpdateFailedError(msg string) {
|
|
fields := []slogger.Field{
|
|
{Key: "opensandbox.event", Value: "egress.update_failed"},
|
|
{Key: "error", Value: msg},
|
|
}
|
|
log.Logger.With(fields...).Errorf("egress policy update failed")
|
|
}
|