feat: support direct network exit for runtimes

This commit is contained in:
2026-09-01 19:12:48 +08:00
parent 5674cfe8c3
commit 3729ce910f
15 changed files with 259 additions and 144 deletions
+1 -1
View File
@@ -124,7 +124,7 @@ func decodeRestrictedBrowserRequest(body []byte, target any) error {
func validDouyinGeneration(input douyinGenerationRequest) bool {
return input.BindingVersion > 0 && exitIDPattern.MatchString(input.RuntimeID) &&
exitIDPattern.MatchString(input.NetworkID) && exitIDPattern.MatchString(input.NetworkExitID)
exitIDPattern.MatchString(input.NetworkID) && (input.NetworkExitID == "" || exitIDPattern.MatchString(input.NetworkExitID))
}
func (api gateway) requireDouyinGeneration(alias string, input douyinGenerationRequest) error {
+23 -15
View File
@@ -353,6 +353,7 @@ func (api gateway) list(c fiber.Ctx) error {
}
bindingVersion, _ := strconv.ParseInt(container.Labels[bindingVersionLabel], 10, 64)
proxyPort, _ := strconv.Atoi(container.Labels[proxyPortLabel])
direct := container.Labels[networkExitLabel] == ""
browsers = append(browsers, browser{
ID: container.ID,
Alias: alias,
@@ -363,7 +364,7 @@ func (api gateway) list(c fiber.Ctx) error {
BindingVersion: bindingVersion,
NetworkExitID: container.Labels[networkExitLabel],
NetworkID: container.Labels[networkIDLabel],
ProxyReady: api.proxies.ready(alias, proxyPort, container.ID, container.Labels[networkIDLabel]),
ProxyReady: direct || api.proxies.ready(alias, proxyPort, container.ID, container.Labels[networkIDLabel]),
})
}
return writeJSON(c, http.StatusOK, browsers)
@@ -397,6 +398,7 @@ func (api gateway) create(c fiber.Ctx) error {
} else if !errors.Is(err, os.ErrNotExist) {
return writeError(c, statusFor(err), err)
}
direct := input.NetworkExitID == ""
network, proxyServer, undoProxy := "none", "", func() {}
var networkGeneration tenantNetworkGeneration
keepNetwork := input.Stopped
@@ -425,9 +427,11 @@ func (api gateway) create(c fiber.Ctx) error {
return writeNetworkError(c, http.StatusBadGateway, errors.New("configure isolated browser network"), networkGeneration.ID)
}
network = networkGeneration.ID
proxyServer, undoProxy, err = api.proxies.configure(input.Alias, input.BindingVersion, bindHost, 0, input.NetworkExit, networkGeneration.ID)
if err != nil {
return writeNetworkError(c, statusFor(err), errors.Join(errors.New("configure in-memory proxy"), err), networkGeneration.ID)
if !direct {
proxyServer, undoProxy, err = api.proxies.configure(input.Alias, input.BindingVersion, bindHost, 0, input.NetworkExit, networkGeneration.ID)
if err != nil {
return writeNetworkError(c, statusFor(err), errors.Join(errors.New("configure in-memory proxy"), err), networkGeneration.ID)
}
}
}
keepProxy := false
@@ -439,7 +443,7 @@ func (api gateway) create(c fiber.Ctx) error {
pidsLimit := int64(512)
cmd := append([]string{}, input.Cmd...)
if !input.Stopped {
if !input.Stopped && !direct {
cmd = append(cmd[:len(cmd)-1], "--proxy-server="+proxyServer, "--disable-non-proxied-udp", cmd[len(cmd)-1])
}
payload := map[string]any{
@@ -508,14 +512,16 @@ func (api gateway) create(c fiber.Ctx) error {
}
}
if !input.Stopped {
if !api.proxies.bind(input.Alias, input.BindingVersion, proxyServer, created.ID, networkGeneration.ID) {
cleanupErr := api.docker.expect(http.MethodDelete, "/containers/"+url.PathEscape(created.ID)+"?force=1&v=0", nil, http.StatusNoContent)
if cleanupErr != nil {
return writeNetworkError(c, http.StatusBadGateway, fmt.Errorf("proxy generation changed and container cleanup failed: %w", cleanupErr), networkGeneration.ID)
if !direct {
if !api.proxies.bind(input.Alias, input.BindingVersion, proxyServer, created.ID, networkGeneration.ID) {
cleanupErr := api.docker.expect(http.MethodDelete, "/containers/"+url.PathEscape(created.ID)+"?force=1&v=0", nil, http.StatusNoContent)
if cleanupErr != nil {
return writeNetworkError(c, http.StatusBadGateway, fmt.Errorf("proxy generation changed and container cleanup failed: %w", cleanupErr), networkGeneration.ID)
}
return writeNetworkError(c, http.StatusConflict, errGenerationConflict, networkGeneration.ID)
}
return writeNetworkError(c, http.StatusConflict, errGenerationConflict, networkGeneration.ID)
undoProxy = func() { api.proxies.remove(input.Alias, input.BindingVersion, created.ID) }
}
undoProxy = func() { api.proxies.remove(input.Alias, input.BindingVersion, created.ID) }
if err := api.docker.expect(http.MethodPost, "/containers/"+url.PathEscape(created.ID)+"/start", nil, http.StatusNoContent, http.StatusNotModified); err != nil {
cleanupErr := api.docker.expect(http.MethodDelete, "/containers/"+url.PathEscape(created.ID)+"?force=1&v=0", nil, http.StatusNoContent)
if cleanupErr != nil {
@@ -524,7 +530,7 @@ func (api gateway) create(c fiber.Ctx) error {
return writeNetworkError(c, http.StatusBadGateway, fmt.Errorf("container did not start and was removed while preserving its Profile volume: %w", err), networkGeneration.ID)
}
}
keepProxy, keepNetwork = !input.Stopped, true
keepProxy, keepNetwork = !input.Stopped && !direct, true
return writeJSON(c, http.StatusCreated, map[string]string{"id": created.ID, "alias": input.Alias, "network_id": networkGeneration.ID})
}
@@ -541,8 +547,10 @@ func validateCreate(input createRequest) error {
if !volumePattern.MatchString(input.Volume) {
return errors.New("volume must be a valid volume name")
}
if input.BindingVersion < 1 || (!input.Stopped && !exitIDPattern.MatchString(input.NetworkExitID)) ||
(input.Stopped && (input.NetworkExitID != "" || input.NetworkExit != (gatewayProxyExit{}))) {
direct := input.NetworkExitID == "" && input.NetworkExit == (gatewayProxyExit{})
if input.BindingVersion < 1 || (input.Stopped && !direct) ||
(!input.Stopped && !direct && !exitIDPattern.MatchString(input.NetworkExitID)) ||
(input.NetworkExitID == "") != (input.NetworkExit == (gatewayProxyExit{})) {
return errors.New("binding_version and network_exit_id must identify the current binding")
}
if len(input.Cmd) == 0 || len(input.Cmd) > 64 || input.Cmd[len(input.Cmd)-1] != "about:blank" {
@@ -561,7 +569,7 @@ func validateCreate(input createRequest) error {
if total > 4096 {
return errors.New("cmd arguments exceed 4096 characters")
}
if input.Stopped {
if input.Stopped || direct {
return nil
}
proxy := input.NetworkExit
+44
View File
@@ -572,6 +572,50 @@ func TestGatewayCreatesConstrainedBrowserWithPlatformSpec(t *testing.T) {
}
}
func TestGatewayCreatesDirectBrowserWithoutProxyArguments(t *testing.T) {
var created map[string]any
docker, server := testDocker(func(response http.ResponseWriter, request *http.Request) {
switch {
case request.Method == http.MethodGet && strings.HasPrefix(request.URL.Path, "/images/"):
_, _ = response.Write([]byte(`{}`))
case request.Method == http.MethodGet && strings.HasPrefix(request.URL.Path, "/containers/"+namePrefix):
response.WriteHeader(http.StatusNotFound)
case request.Method == http.MethodPost && strings.HasSuffix(request.URL.Path, "/containers/create"):
if err := json.NewDecoder(request.Body).Decode(&created); err != nil {
t.Fatal(err)
}
response.WriteHeader(http.StatusCreated)
_, _ = response.Write([]byte(`{"Id":"container-id"}`))
case request.Method == http.MethodPost && strings.HasSuffix(request.URL.Path, "/containers/container-id/start"):
response.WriteHeader(http.StatusNoContent)
default:
t.Fatalf("unexpected Docker request %s %s", request.Method, request.URL.String())
}
})
defer server.Close()
handler := newGateway(docker, "creatorhub_browser", testToken)
body := `{"alias":"account-a","name":"账号甲","image":"registry.example/browser:1.2.3",` +
`"cmd":["--fingerprint=1000","about:blank"],"volume":"creatorhub-profile-account-a",` +
`"binding_version":1,"network_exit_id":"","network_exit":{}}`
response := httptest.NewRecorder()
adaptor.FiberApp(handler).ServeHTTP(response, authed(http.MethodPost, "/v1/browsers", strings.NewReader(body)))
if response.Code != http.StatusCreated {
t.Fatalf("expected 201, got %d: %s", response.Code, response.Body.String())
}
cmd := created["Cmd"].([]any)
encoded, _ := json.Marshal(cmd)
if len(cmd) != 2 || strings.Contains(string(encoded), "proxy") {
t.Fatalf("direct runtime received proxy arguments: %#v", cmd)
}
host := created["HostConfig"].(map[string]any)
labels := created["Labels"].(map[string]any)
if host["NetworkMode"] != "network-account-a" || labels[networkExitLabel] != "" || labels[proxyPortLabel] != "0" {
t.Fatalf("direct runtime metadata is invalid: host=%#v labels=%#v", host, labels)
}
}
func TestGatewayCreateUsesCapturedNetworkIDAcrossNameReplacement(t *testing.T) {
networkID := ""
members := map[string]string{}