mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-07 18:26:37 +08:00
fix: harden proxy protocol rollout safety (#494)
This commit is contained in:
@@ -304,6 +304,7 @@ func autoMigrateAll(db *gorm.DB) error {
|
|||||||
m := db.Migrator()
|
m := db.Migrator()
|
||||||
hasNode := m.HasTable(&model.Node{})
|
hasNode := m.HasTable(&model.Node{})
|
||||||
hasTunnel := m.HasTable(&model.Tunnel{})
|
hasTunnel := m.HasTable(&model.Tunnel{})
|
||||||
|
hasForward := m.HasTable(&model.Forward{})
|
||||||
|
|
||||||
for _, item := range models {
|
for _, item := range models {
|
||||||
if hasNode {
|
if hasNode {
|
||||||
@@ -316,6 +317,11 @@ func autoMigrateAll(db *gorm.DB) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if hasForward {
|
||||||
|
if _, ok := item.(*model.Forward); ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
if err := db.AutoMigrate(item); err != nil {
|
if err := db.AutoMigrate(item); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -396,7 +402,7 @@ func prepareSQLiteLegacyColumns(db *gorm.DB) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if m.HasTable(&model.Forward{}) {
|
if m.HasTable(&model.Forward{}) {
|
||||||
for _, field := range []string{"ProxyProtocol"} {
|
for _, field := range []string{"MaxConn", "IPMaxConn", "IPSpeedID", "ProxyProtocol"} {
|
||||||
if m.HasColumn(&model.Forward{}, field) {
|
if m.HasColumn(&model.Forward{}, field) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,6 +117,68 @@ func TestOpenBackfillsSQLiteLegacyTunnelProbeTargetColumns(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOpenBackfillsSQLiteLegacyForwardColumns(t *testing.T) {
|
||||||
|
dbPath := filepath.Join(t.TempDir(), "legacy-forward.db")
|
||||||
|
db, err := gorm.Open(gsqlite.Open(dbPath), &gorm.Config{
|
||||||
|
Logger: logger.Default.LogMode(logger.Silent),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open legacy sqlite: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Exec(`
|
||||||
|
CREATE TABLE forward (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
user_name VARCHAR(100) NOT NULL,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
tunnel_id INTEGER NOT NULL,
|
||||||
|
remote_addr TEXT NOT NULL,
|
||||||
|
strategy VARCHAR(100) NOT NULL DEFAULT 'fifo',
|
||||||
|
in_flow INTEGER NOT NULL DEFAULT 0,
|
||||||
|
out_flow INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_time INTEGER NOT NULL,
|
||||||
|
updated_time INTEGER NOT NULL,
|
||||||
|
status INTEGER NOT NULL,
|
||||||
|
inx INTEGER NOT NULL DEFAULT 0,
|
||||||
|
speed_id INTEGER
|
||||||
|
)
|
||||||
|
`).Error; err != nil {
|
||||||
|
t.Fatalf("create legacy forward table: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Exec(`
|
||||||
|
INSERT INTO forward(id, user_id, user_name, name, tunnel_id, remote_addr, strategy, in_flow, out_flow, created_time, updated_time, status, inx, speed_id)
|
||||||
|
VALUES(1, 2, 'legacy-user', 'legacy-forward', 3, '127.0.0.1:9000', 'fifo', 0, 0, 1, 1, 1, 0, NULL)
|
||||||
|
`).Error; err != nil {
|
||||||
|
t.Fatalf("insert legacy forward: %v", err)
|
||||||
|
}
|
||||||
|
if sqlDB, _ := db.DB(); sqlDB != nil {
|
||||||
|
_ = sqlDB.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := Open(dbPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open migrated sqlite: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = r.Close() })
|
||||||
|
|
||||||
|
m := r.DB().Migrator()
|
||||||
|
for _, field := range []string{"MaxConn", "IPMaxConn", "IPSpeedID", "ProxyProtocol"} {
|
||||||
|
if !m.HasColumn(&model.Forward{}, field) {
|
||||||
|
t.Fatalf("expected forward.%s column to exist", field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var maxConn, ipMaxConn, proxyProtocol int
|
||||||
|
var ipSpeedID sql.NullInt64
|
||||||
|
if err := r.DB().Raw(`SELECT max_conn, ip_max_conn, ip_speed_id, proxy_protocol FROM forward WHERE id = 1`).Row().Scan(&maxConn, &ipMaxConn, &ipSpeedID, &proxyProtocol); err != nil {
|
||||||
|
t.Fatalf("query forward defaults: %v", err)
|
||||||
|
}
|
||||||
|
if maxConn != 0 || ipMaxConn != 0 || ipSpeedID.Valid || proxyProtocol != 0 {
|
||||||
|
t.Fatalf("expected default forward columns 0/0/NULL/0, got max_conn=%d ip_max_conn=%d ip_speed_id=%+v proxy_protocol=%d", maxConn, ipMaxConn, ipSpeedID, proxyProtocol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMigrateSchemaRunsPostgresIDRepairEvenAtCurrentVersion(t *testing.T) {
|
func TestMigrateSchemaRunsPostgresIDRepairEvenAtCurrentVersion(t *testing.T) {
|
||||||
db, err := gorm.Open(gsqlite.Open(":memory:"), &gorm.Config{
|
db, err := gorm.Open(gsqlite.Open(":memory:"), &gorm.Config{
|
||||||
Logger: logger.Default.LogMode(logger.Silent),
|
Logger: logger.Default.LogMode(logger.Silent),
|
||||||
|
|||||||
@@ -158,6 +158,9 @@ type WebSocketReporter struct {
|
|||||||
addr string // 保存服务器地址
|
addr string // 保存服务器地址
|
||||||
secret string // 保存密钥
|
secret string // 保存密钥
|
||||||
version string // 保存版本号
|
version string // 保存版本号
|
||||||
|
http int
|
||||||
|
tls int
|
||||||
|
socks int
|
||||||
preferredWSScheme string
|
preferredWSScheme string
|
||||||
conn *websocket.Conn
|
conn *websocket.Conn
|
||||||
curBackoff time.Duration // 当前重连退避间隔
|
curBackoff time.Duration // 当前重连退避间隔
|
||||||
@@ -296,9 +299,9 @@ func (w *WebSocketReporter) connect() error {
|
|||||||
Socks int `json:"socks"`
|
Socks int `json:"socks"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var cfg LocalConfig
|
cfg := LocalConfig{Http: w.http, Tls: w.tls, Socks: w.socks}
|
||||||
if b, err := os.ReadFile("config.json"); err == nil {
|
if b, err := os.ReadFile("config.json"); err == nil {
|
||||||
json.Unmarshal(b, &cfg)
|
_ = json.Unmarshal(b, &cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
candidates := buildWebSocketCandidates(w.addr, w.secret, w.version, cfg.Http, cfg.Tls, cfg.Socks, w.preferredWSScheme)
|
candidates := buildWebSocketCandidates(w.addr, w.secret, w.version, cfg.Http, cfg.Tls, cfg.Socks, w.preferredWSScheme)
|
||||||
@@ -1369,7 +1372,7 @@ func (w *WebSocketReporter) handleUpgradeAgent(data interface{}) error {
|
|||||||
// 执行重启脚本
|
// 执行重启脚本
|
||||||
// 使用 systemd-run 在独立的 transient unit 中运行重启脚本,
|
// 使用 systemd-run 在独立的 transient unit 中运行重启脚本,
|
||||||
// 避免 systemctl stop 杀死 flux_agent cgroup 内所有进程(包括此脚本自身)导致 mv 未执行。
|
// 避免 systemctl stop 杀死 flux_agent cgroup 内所有进程(包括此脚本自身)导致 mv 未执行。
|
||||||
script := fmt.Sprintf("sleep 1 && systemctl stop flux_agent && mv %s %s && systemctl start flux_agent", tmpPath, binaryPath)
|
script := buildAgentRestartScript(tmpPath, binaryPath)
|
||||||
cmd := exec.Command("systemd-run", "--quiet", "/bin/sh", "-c", script)
|
cmd := exec.Command("systemd-run", "--quiet", "/bin/sh", "-c", script)
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
os.Remove(tmpPath)
|
os.Remove(tmpPath)
|
||||||
@@ -1403,6 +1406,14 @@ func (w *WebSocketReporter) handleRollbackAgent(data interface{}) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildAgentRestartScript(tmpPath, binaryPath string) string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"sleep 1 && systemctl stop flux_agent && legacy_service='' && for service_file in /etc/systemd/system/gost.service /lib/systemd/system/gost.service /usr/lib/systemd/system/gost.service; do if [ -f \"$service_file\" ] && grep -Fq \"WorkingDirectory=/etc/gost\" \"$service_file\" && (grep -Fq \"ExecStart=/etc/gost/gost\" \"$service_file\" || (grep -Fq \"ExecStart=/usr/local/bin/gost\" \"$service_file\" && [ -f /etc/gost/config.json ] && [ -f /etc/gost/gost.json ])); then legacy_service=\"$service_file\"; break; fi; done && if [ -n \"$legacy_service\" ]; then (systemctl stop gost 2>/dev/null || true) && (systemctl disable gost 2>/dev/null || true) && rm -f /usr/local/bin/gost /etc/gost/gost \"$legacy_service\" && (systemctl daemon-reload 2>/dev/null || true); fi && mv %s %s && systemctl start flux_agent",
|
||||||
|
tmpPath,
|
||||||
|
binaryPath,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// updateLocalConfigJSON 将 http/tls/socks 写入工作目录下的 config.json
|
// updateLocalConfigJSON 将 http/tls/socks 写入工作目录下的 config.json
|
||||||
func updateLocalConfigJSON(httpVal int, tlsVal int, socksVal int) error {
|
func updateLocalConfigJSON(httpVal int, tlsVal int, socksVal int) error {
|
||||||
path := "config.json"
|
path := "config.json"
|
||||||
@@ -1650,13 +1661,16 @@ func StartWebSocketReporterWithConfig(addr string, secret string, http int, tls
|
|||||||
candidates := buildWebSocketCandidates(addr, secret, version, http, tls, socks, "")
|
candidates := buildWebSocketCandidates(addr, secret, version, http, tls, socks, "")
|
||||||
fullURL := candidates[0]
|
fullURL := candidates[0]
|
||||||
|
|
||||||
fmt.Printf("🔗 WebSocket连接URL: %s\n", fullURL)
|
fmt.Printf("🔗 WebSocket连接URL: %s\n", sanitizeWebSocketURL(fullURL))
|
||||||
|
|
||||||
reporter := NewWebSocketReporter(fullURL, secret)
|
reporter := NewWebSocketReporter(fullURL, secret)
|
||||||
// 保存 addr, secret, version 供重连时使用
|
// 保存 addr, secret, version 和协议能力供重连时使用
|
||||||
reporter.addr = addr
|
reporter.addr = addr
|
||||||
reporter.secret = secret
|
reporter.secret = secret
|
||||||
reporter.version = version
|
reporter.version = version
|
||||||
|
reporter.http = http
|
||||||
|
reporter.tls = tls
|
||||||
|
reporter.socks = socks
|
||||||
reporter.Start()
|
reporter.Start()
|
||||||
return reporter
|
return reporter
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,45 @@
|
|||||||
package socket
|
package socket
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func captureStdout(t *testing.T, fn func()) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
orig := os.Stdout
|
||||||
|
r, w, err := os.Pipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create stdout pipe: %v", err)
|
||||||
|
}
|
||||||
|
os.Stdout = w
|
||||||
|
defer func() {
|
||||||
|
os.Stdout = orig
|
||||||
|
_ = w.Close()
|
||||||
|
_ = r.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
fn()
|
||||||
|
|
||||||
|
_ = w.Close()
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if _, err := io.Copy(&buf, r); err != nil {
|
||||||
|
t.Fatalf("read stdout: %v", err)
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
func TestBuildWebSocketCandidatesSecureFirst(t *testing.T) {
|
func TestBuildWebSocketCandidatesSecureFirst(t *testing.T) {
|
||||||
candidates := buildWebSocketCandidates("panel.example.com:443", "abc", "2.0.2", 1, 0, 1, "")
|
candidates := buildWebSocketCandidates("panel.example.com:443", "abc", "2.0.2", 1, 0, 1, "")
|
||||||
|
|
||||||
@@ -133,3 +163,103 @@ func TestFormatWebSocketDialErrorIncludesHTTPStatus(t *testing.T) {
|
|||||||
t.Fatalf("expected response body in message, got %s", msg)
|
t.Fatalf("expected response body in message, got %s", msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAgentUpgradeRestartScriptStopsLegacyGostService(t *testing.T) {
|
||||||
|
script := buildAgentRestartScript("/tmp/flux_agent.new", "/etc/flux_agent/flux_agent")
|
||||||
|
|
||||||
|
if !strings.Contains(script, "systemctl stop flux_agent") {
|
||||||
|
t.Fatalf("expected script to stop flux_agent, got %s", script)
|
||||||
|
}
|
||||||
|
if !strings.Contains(script, "mv /tmp/flux_agent.new /etc/flux_agent/flux_agent") {
|
||||||
|
t.Fatalf("expected script to replace the flux_agent binary, got %s", script)
|
||||||
|
}
|
||||||
|
if !strings.Contains(script, "systemctl stop gost") {
|
||||||
|
t.Fatalf("expected script to stop the legacy gost service, got %s", script)
|
||||||
|
}
|
||||||
|
if !strings.Contains(script, "systemctl disable gost") {
|
||||||
|
t.Fatalf("expected script to disable the legacy gost service, got %s", script)
|
||||||
|
}
|
||||||
|
if !strings.Contains(script, "rm -f /usr/local/bin/gost") {
|
||||||
|
t.Fatalf("expected script to remove the legacy gost binary, got %s", script)
|
||||||
|
}
|
||||||
|
if !strings.Contains(script, "WorkingDirectory=/etc/gost") {
|
||||||
|
t.Fatalf("expected script to scope cleanup to the legacy FLVX gost service definition, got %s", script)
|
||||||
|
}
|
||||||
|
if !strings.Contains(script, "systemctl start flux_agent") {
|
||||||
|
t.Fatalf("expected script to restart flux_agent, got %s", script)
|
||||||
|
}
|
||||||
|
if strings.Contains(script, "systemctl stop flux_agent && systemctl stop gost 2>/dev/null || true") {
|
||||||
|
t.Fatalf("expected legacy gost cleanup fallback to be scoped, got %s", script)
|
||||||
|
}
|
||||||
|
if runtime.GOARCH == "" {
|
||||||
|
t.Fatalf("unexpected empty runtime arch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStartWebSocketReporterWithConfigPreservesProtocolDefaultsWithoutConfigFile(t *testing.T) {
|
||||||
|
origDial := wsDial
|
||||||
|
defer func() { wsDial = origDial }()
|
||||||
|
|
||||||
|
origWD, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get working directory: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_ = os.Chdir(origWD)
|
||||||
|
})
|
||||||
|
if err := os.Chdir(t.TempDir()); err != nil {
|
||||||
|
t.Fatalf("change working directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
urls := make(chan string, 1)
|
||||||
|
wsDial = func(_ *websocket.Dialer, rawURL string) (*websocket.Conn, *http.Response, error) {
|
||||||
|
select {
|
||||||
|
case urls <- rawURL:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return nil, nil, errors.New("dial failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
reporter := StartWebSocketReporterWithConfig("panel.example.com:443", "abc", 1, 0, 1, "2.0.2")
|
||||||
|
defer reporter.Stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case rawURL := <-urls:
|
||||||
|
if !strings.Contains(rawURL, "http=1&tls=0&socks=1") {
|
||||||
|
t.Fatalf("expected reconnect URL to preserve startup protocol values, got %s", rawURL)
|
||||||
|
}
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("timed out waiting for websocket dial")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStartWebSocketReporterWithConfigLogsSanitizedURL(t *testing.T) {
|
||||||
|
origDial := wsDial
|
||||||
|
defer func() { wsDial = origDial }()
|
||||||
|
|
||||||
|
ready := make(chan struct{}, 1)
|
||||||
|
wsDial = func(_ *websocket.Dialer, rawURL string) (*websocket.Conn, *http.Response, error) {
|
||||||
|
select {
|
||||||
|
case ready <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return nil, nil, errors.New("dial failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
output := captureStdout(t, func() {
|
||||||
|
reporter := StartWebSocketReporterWithConfig("panel.example.com:443", "abc123", 1, 0, 1, "2.0.2")
|
||||||
|
select {
|
||||||
|
case <-ready:
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("timed out waiting for websocket dial")
|
||||||
|
}
|
||||||
|
reporter.Stop()
|
||||||
|
})
|
||||||
|
|
||||||
|
if strings.Contains(output, "secret=abc123") {
|
||||||
|
t.Fatalf("expected logged websocket URL to mask the node secret, got %s", output)
|
||||||
|
}
|
||||||
|
if !strings.Contains(output, "secret=%2A%2A%2A") {
|
||||||
|
t.Fatalf("expected logged websocket URL to include masked secret, got %s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+79
-11
@@ -24,6 +24,11 @@ get_architecture() {
|
|||||||
|
|
||||||
# 安装目录
|
# 安装目录
|
||||||
INSTALL_DIR="/etc/flux_agent"
|
INSTALL_DIR="/etc/flux_agent"
|
||||||
|
LEGACY_GOST_BINARY="/usr/local/bin/gost"
|
||||||
|
LEGACY_GOST_CONFIG_DIR="/etc/gost"
|
||||||
|
LEGACY_GOST_SERVICE_FILE_ETC="/etc/systemd/system/gost.service"
|
||||||
|
LEGACY_GOST_SERVICE_FILE_LIB="/lib/systemd/system/gost.service"
|
||||||
|
LEGACY_GOST_SERVICE_FILE_USR_LIB="/usr/lib/systemd/system/gost.service"
|
||||||
|
|
||||||
# 镜像加速配置(可由面板传入或交互式询问)
|
# 镜像加速配置(可由面板传入或交互式询问)
|
||||||
PROXY_ENABLED="${PROXY_ENABLED:-}"
|
PROXY_ENABLED="${PROXY_ENABLED:-}"
|
||||||
@@ -234,6 +239,69 @@ check_and_install_tcpkill() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
json_escape() {
|
||||||
|
local value="$1"
|
||||||
|
value=${value//\\/\\\\}
|
||||||
|
value=${value//\"/\\\"}
|
||||||
|
value=${value//$'\n'/\\n}
|
||||||
|
value=${value//$'\r'/\\r}
|
||||||
|
value=${value//$'\t'/\\t}
|
||||||
|
printf '%s' "$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
write_flux_agent_config() {
|
||||||
|
local path="$1"
|
||||||
|
printf '{\n "addr": "%s",\n "secret": "%s"\n}\n' \
|
||||||
|
"$(json_escape "$SERVER_ADDR")" \
|
||||||
|
"$(json_escape "$SECRET")" > "$path"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_legacy_gost_installation() {
|
||||||
|
local matched_service_files=()
|
||||||
|
local service_file=""
|
||||||
|
local removed_service_file="0"
|
||||||
|
|
||||||
|
for service_file in "$LEGACY_GOST_SERVICE_FILE_ETC" "$LEGACY_GOST_SERVICE_FILE_LIB" "$LEGACY_GOST_SERVICE_FILE_USR_LIB"; do
|
||||||
|
if [[ ! -f "$service_file" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if ! grep -Fq "WorkingDirectory=$LEGACY_GOST_CONFIG_DIR" "$service_file"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if grep -Fq "ExecStart=$LEGACY_GOST_CONFIG_DIR/gost" "$service_file" || \
|
||||||
|
(grep -Fq "ExecStart=$LEGACY_GOST_BINARY" "$service_file" && [[ -f "$LEGACY_GOST_CONFIG_DIR/config.json" && -f "$LEGACY_GOST_CONFIG_DIR/gost.json" ]]); then
|
||||||
|
matched_service_files+=("$service_file")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ${#matched_service_files[@]} -eq 0 ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if systemctl list-units --full -all 2>/dev/null | grep -Fq "gost.service"; then
|
||||||
|
systemctl stop gost 2>/dev/null || true
|
||||||
|
systemctl disable gost 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
for service_file in "${matched_service_files[@]}"; do
|
||||||
|
if [[ -f "$service_file" ]]; then
|
||||||
|
rm -f "$service_file"
|
||||||
|
removed_service_file="1"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -f "$LEGACY_GOST_BINARY" ]]; then
|
||||||
|
rm -f "$LEGACY_GOST_BINARY"
|
||||||
|
fi
|
||||||
|
if [[ -f "$LEGACY_GOST_CONFIG_DIR/gost" ]]; then
|
||||||
|
rm -f "$LEGACY_GOST_CONFIG_DIR/gost"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$removed_service_file" == "1" ]]; then
|
||||||
|
systemctl daemon-reload 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# 获取用户输入的配置参数
|
# 获取用户输入的配置参数
|
||||||
get_config_params() {
|
get_config_params() {
|
||||||
@@ -279,6 +347,8 @@ install_flux_agent() {
|
|||||||
|
|
||||||
mkdir -p "$INSTALL_DIR"
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
|
||||||
|
local tmp_binary="$INSTALL_DIR/flux_agent.new"
|
||||||
|
|
||||||
# 停止并禁用已有服务
|
# 停止并禁用已有服务
|
||||||
if systemctl list-units --full -all | grep -Fq "flux_agent.service"; then
|
if systemctl list-units --full -all | grep -Fq "flux_agent.service"; then
|
||||||
echo "🔍 检测到已存在的flux_agent服务"
|
echo "🔍 检测到已存在的flux_agent服务"
|
||||||
@@ -286,16 +356,17 @@ install_flux_agent() {
|
|||||||
systemctl disable flux_agent 2>/dev/null && echo "🚫 禁用自启"
|
systemctl disable flux_agent 2>/dev/null && echo "🚫 禁用自启"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 删除旧文件
|
|
||||||
[[ -f "$INSTALL_DIR/flux_agent" ]] && echo "🧹 删除旧文件 flux_agent" && rm -f "$INSTALL_DIR/flux_agent"
|
|
||||||
|
|
||||||
# 下载 flux_agent
|
# 下载 flux_agent
|
||||||
echo "⬇️ 下载 flux_agent 中..."
|
echo "⬇️ 下载 flux_agent 中..."
|
||||||
curl -L "$DOWNLOAD_URL" -o "$INSTALL_DIR/flux_agent"
|
rm -f "$tmp_binary"
|
||||||
if [[ ! -f "$INSTALL_DIR/flux_agent" || ! -s "$INSTALL_DIR/flux_agent" ]]; then
|
curl -L "$DOWNLOAD_URL" -o "$tmp_binary"
|
||||||
|
if [[ ! -f "$tmp_binary" || ! -s "$tmp_binary" ]]; then
|
||||||
|
rm -f "$tmp_binary"
|
||||||
echo "❌ 下载失败,请检查网络或下载链接。"
|
echo "❌ 下载失败,请检查网络或下载链接。"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
cleanup_legacy_gost_installation
|
||||||
|
mv "$tmp_binary" "$INSTALL_DIR/flux_agent"
|
||||||
chmod +x "$INSTALL_DIR/flux_agent"
|
chmod +x "$INSTALL_DIR/flux_agent"
|
||||||
echo "✅ 下载完成"
|
echo "✅ 下载完成"
|
||||||
|
|
||||||
@@ -305,12 +376,7 @@ install_flux_agent() {
|
|||||||
# 写入 config.json (安装时总是创建新的)
|
# 写入 config.json (安装时总是创建新的)
|
||||||
CONFIG_FILE="$INSTALL_DIR/config.json"
|
CONFIG_FILE="$INSTALL_DIR/config.json"
|
||||||
echo "📄 创建新配置: config.json"
|
echo "📄 创建新配置: config.json"
|
||||||
cat > "$CONFIG_FILE" <<EOF
|
write_flux_agent_config "$CONFIG_FILE"
|
||||||
{
|
|
||||||
"addr": "$SERVER_ADDR",
|
|
||||||
"secret": "$SECRET"
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# 写入 gost.json
|
# 写入 gost.json
|
||||||
GOST_CONFIG="$INSTALL_DIR/gost.json"
|
GOST_CONFIG="$INSTALL_DIR/gost.json"
|
||||||
@@ -380,11 +446,13 @@ update_flux_agent() {
|
|||||||
|
|
||||||
# 先下载新版本
|
# 先下载新版本
|
||||||
echo "⬇️ 下载最新版本..."
|
echo "⬇️ 下载最新版本..."
|
||||||
|
rm -f "$INSTALL_DIR/flux_agent.new"
|
||||||
curl -L "$DOWNLOAD_URL" -o "$INSTALL_DIR/flux_agent.new"
|
curl -L "$DOWNLOAD_URL" -o "$INSTALL_DIR/flux_agent.new"
|
||||||
if [[ ! -f "$INSTALL_DIR/flux_agent.new" || ! -s "$INSTALL_DIR/flux_agent.new" ]]; then
|
if [[ ! -f "$INSTALL_DIR/flux_agent.new" || ! -s "$INSTALL_DIR/flux_agent.new" ]]; then
|
||||||
echo "❌ 下载失败。"
|
echo "❌ 下载失败。"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
cleanup_legacy_gost_installation
|
||||||
|
|
||||||
# 停止服务
|
# 停止服务
|
||||||
if systemctl list-units --full -all | grep -Fq "flux_agent.service"; then
|
if systemctl list-units --full -all | grep -Fq "flux_agent.service"; then
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ EOF
|
|||||||
chmod +x "$INSTALL_DIR/flux_agent"
|
chmod +x "$INSTALL_DIR/flux_agent"
|
||||||
|
|
||||||
local ask_called="0"
|
local ask_called="0"
|
||||||
|
local cleanup_called="0"
|
||||||
|
|
||||||
ask_proxy_config() {
|
ask_proxy_config() {
|
||||||
ask_called="1"
|
ask_called="1"
|
||||||
@@ -105,6 +106,10 @@ EOF
|
|||||||
DOWNLOAD_URL=""
|
DOWNLOAD_URL=""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cleanup_legacy_gost_installation() {
|
||||||
|
cleanup_called="1"
|
||||||
|
}
|
||||||
|
|
||||||
check_and_install_tcpkill() { :; }
|
check_and_install_tcpkill() { :; }
|
||||||
|
|
||||||
systemctl() {
|
systemctl() {
|
||||||
@@ -133,6 +138,7 @@ EOF
|
|||||||
update_flux_agent >/dev/null
|
update_flux_agent >/dev/null
|
||||||
|
|
||||||
assert_equals "1" "$ask_called" "update_flux_agent should ask for proxy config before downloading"
|
assert_equals "1" "$ask_called" "update_flux_agent should ask for proxy config before downloading"
|
||||||
|
assert_equals "1" "$cleanup_called" "update_flux_agent should clean up legacy gost before restarting the agent"
|
||||||
assert_equals "$(build_download_url)" "$DOWNLOAD_URL" "update_flux_agent should honor the prompted proxy choice"
|
assert_equals "$(build_download_url)" "$DOWNLOAD_URL" "update_flux_agent should honor the prompted proxy choice"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -155,6 +161,198 @@ test_update_flux_agent_skips_proxy_prompt_when_not_installed() (
|
|||||||
assert_equals "0" "$ask_called" "update_flux_agent should not prompt for proxy config when the agent is missing"
|
assert_equals "0" "$ask_called" "update_flux_agent should not prompt for proxy config when the agent is missing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
test_install_flux_agent_preserves_legacy_gost_when_download_fails() (
|
||||||
|
set -euo pipefail
|
||||||
|
load_script_without_main "$ROOT_DIR/install.sh"
|
||||||
|
|
||||||
|
INSTALL_DIR=$(mktemp -d)
|
||||||
|
cat > "$INSTALL_DIR/flux_agent" <<'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
echo "old version"
|
||||||
|
EOF
|
||||||
|
chmod +x "$INSTALL_DIR/flux_agent"
|
||||||
|
SERVER_ADDR="panel.example.com:443"
|
||||||
|
SECRET="secret"
|
||||||
|
DOWNLOAD_URL="https://example.com/gost"
|
||||||
|
|
||||||
|
local cleanup_called="0"
|
||||||
|
local rc="0"
|
||||||
|
|
||||||
|
ask_proxy_config() { :; }
|
||||||
|
ensure_download_url_initialized() { :; }
|
||||||
|
get_config_params() { :; }
|
||||||
|
check_and_install_tcpkill() { :; }
|
||||||
|
cleanup_legacy_gost_installation() {
|
||||||
|
cleanup_called="1"
|
||||||
|
}
|
||||||
|
systemctl() { return 0; }
|
||||||
|
curl() { return 0; }
|
||||||
|
|
||||||
|
( install_flux_agent >/dev/null ) || rc="$?"
|
||||||
|
|
||||||
|
assert_equals "1" "$rc" "install_flux_agent should fail when the download artifact is missing"
|
||||||
|
assert_equals "0" "$cleanup_called" "install_flux_agent should preserve legacy gost when download fails"
|
||||||
|
[[ -f "$INSTALL_DIR/flux_agent" ]] || fail "install_flux_agent should keep the existing flux_agent binary when download fails"
|
||||||
|
)
|
||||||
|
|
||||||
|
test_update_flux_agent_preserves_legacy_gost_when_download_fails() (
|
||||||
|
set -euo pipefail
|
||||||
|
load_script_without_main "$ROOT_DIR/install.sh"
|
||||||
|
|
||||||
|
INSTALL_DIR=$(mktemp -d)
|
||||||
|
cat > "$INSTALL_DIR/flux_agent" <<'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
echo "old version"
|
||||||
|
EOF
|
||||||
|
chmod +x "$INSTALL_DIR/flux_agent"
|
||||||
|
cat > "$INSTALL_DIR/flux_agent.new" <<'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
echo "stale version"
|
||||||
|
EOF
|
||||||
|
chmod +x "$INSTALL_DIR/flux_agent.new"
|
||||||
|
|
||||||
|
local cleanup_called="0"
|
||||||
|
local rc="0"
|
||||||
|
|
||||||
|
ask_proxy_config() {
|
||||||
|
PROXY_ENABLED="false"
|
||||||
|
DOWNLOAD_URL="https://example.com/gost"
|
||||||
|
}
|
||||||
|
check_and_install_tcpkill() { :; }
|
||||||
|
cleanup_legacy_gost_installation() {
|
||||||
|
cleanup_called="1"
|
||||||
|
}
|
||||||
|
systemctl() { return 0; }
|
||||||
|
curl() { return 0; }
|
||||||
|
|
||||||
|
update_flux_agent >/dev/null || rc="$?"
|
||||||
|
|
||||||
|
assert_equals "1" "$rc" "update_flux_agent should fail when the download artifact is missing"
|
||||||
|
assert_equals "0" "$cleanup_called" "update_flux_agent should preserve legacy gost when download fails"
|
||||||
|
[[ ! -f "$INSTALL_DIR/flux_agent.new" ]] || fail "update_flux_agent should remove stale download artifacts before retrying"
|
||||||
|
)
|
||||||
|
|
||||||
|
test_install_flux_agent_writes_json_safe_config() (
|
||||||
|
set -euo pipefail
|
||||||
|
load_script_without_main "$ROOT_DIR/install.sh"
|
||||||
|
|
||||||
|
INSTALL_DIR=$(mktemp -d)
|
||||||
|
SERVER_ADDR='panel"addr'
|
||||||
|
SECRET='sec\ret"1'
|
||||||
|
DOWNLOAD_URL="https://example.com/gost"
|
||||||
|
|
||||||
|
ask_proxy_config() { :; }
|
||||||
|
ensure_download_url_initialized() { :; }
|
||||||
|
get_config_params() { :; }
|
||||||
|
check_and_install_tcpkill() { :; }
|
||||||
|
cleanup_legacy_gost_installation() { :; }
|
||||||
|
systemctl() { return 0; }
|
||||||
|
curl() {
|
||||||
|
local output=""
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
if [[ "$1" == "-o" ]]; then
|
||||||
|
output="$2"
|
||||||
|
shift 2
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
cat > "$output" <<'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
echo "new version"
|
||||||
|
EOF
|
||||||
|
chmod +x "$output"
|
||||||
|
}
|
||||||
|
|
||||||
|
( install_flux_agent >/dev/null 2>/dev/null ) || true
|
||||||
|
|
||||||
|
local actual
|
||||||
|
actual=$(<"$INSTALL_DIR/config.json")
|
||||||
|
local expected=$'{\n "addr": "panel\\"addr",\n "secret": "sec\\\\ret\\"1"\n}'
|
||||||
|
|
||||||
|
assert_equals "$expected" "$actual" "install_flux_agent should JSON-escape config values"
|
||||||
|
)
|
||||||
|
|
||||||
|
test_cleanup_legacy_gost_installation_removes_service_and_binary() (
|
||||||
|
set -euo pipefail
|
||||||
|
load_script_without_main "$ROOT_DIR/install.sh"
|
||||||
|
|
||||||
|
LEGACY_GOST_BINARY=$(mktemp)
|
||||||
|
LEGACY_GOST_SERVICE_FILE_ETC=$(mktemp)
|
||||||
|
LEGACY_GOST_SERVICE_FILE_LIB=$(mktemp -u)
|
||||||
|
LEGACY_GOST_SERVICE_FILE_USR_LIB=$(mktemp -u)
|
||||||
|
LEGACY_GOST_CONFIG_DIR=$(mktemp -d)
|
||||||
|
cat > "$LEGACY_GOST_SERVICE_FILE_ETC" <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=Gost Proxy Service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
WorkingDirectory=$LEGACY_GOST_CONFIG_DIR
|
||||||
|
ExecStart=$LEGACY_GOST_CONFIG_DIR/gost
|
||||||
|
EOF
|
||||||
|
: > "$LEGACY_GOST_CONFIG_DIR/config.json"
|
||||||
|
: > "$LEGACY_GOST_CONFIG_DIR/gost.json"
|
||||||
|
|
||||||
|
local systemctl_calls=""
|
||||||
|
|
||||||
|
systemctl() {
|
||||||
|
systemctl_calls+=$'\n'"$*"
|
||||||
|
if [[ "$1" == "list-units" ]]; then
|
||||||
|
printf 'gost.service loaded active running\n'
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_legacy_gost_installation >/dev/null
|
||||||
|
|
||||||
|
if [[ -e "$LEGACY_GOST_BINARY" ]]; then
|
||||||
|
fail "cleanup_legacy_gost_installation should remove the legacy gost binary"
|
||||||
|
fi
|
||||||
|
if [[ -e "$LEGACY_GOST_SERVICE_FILE_ETC" ]]; then
|
||||||
|
fail "cleanup_legacy_gost_installation should remove the legacy gost service file"
|
||||||
|
fi
|
||||||
|
[[ "$systemctl_calls" == *"stop gost"* ]] || fail "cleanup_legacy_gost_installation should stop the legacy gost service"
|
||||||
|
[[ "$systemctl_calls" == *"disable gost"* ]] || fail "cleanup_legacy_gost_installation should disable the legacy gost service"
|
||||||
|
[[ "$systemctl_calls" == *"daemon-reload"* ]] || fail "cleanup_legacy_gost_installation should reload systemd after removing the legacy service"
|
||||||
|
)
|
||||||
|
|
||||||
|
test_cleanup_legacy_gost_installation_preserves_unrelated_gost() (
|
||||||
|
set -euo pipefail
|
||||||
|
load_script_without_main "$ROOT_DIR/install.sh"
|
||||||
|
|
||||||
|
LEGACY_GOST_BINARY=$(mktemp)
|
||||||
|
LEGACY_GOST_SERVICE_FILE_ETC=$(mktemp)
|
||||||
|
LEGACY_GOST_SERVICE_FILE_LIB=$(mktemp -u)
|
||||||
|
LEGACY_GOST_SERVICE_FILE_USR_LIB=$(mktemp -u)
|
||||||
|
LEGACY_GOST_CONFIG_DIR=$(mktemp -d)
|
||||||
|
cat > "$LEGACY_GOST_SERVICE_FILE_ETC" <<'EOF'
|
||||||
|
[Unit]
|
||||||
|
Description=Unrelated Gost Service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
WorkingDirectory=/srv/custom-gost
|
||||||
|
ExecStart=/usr/local/bin/gost -C /srv/custom-gost/gost.yaml
|
||||||
|
EOF
|
||||||
|
|
||||||
|
local systemctl_calls=""
|
||||||
|
|
||||||
|
systemctl() {
|
||||||
|
systemctl_calls+=$'\n'"$*"
|
||||||
|
if [[ "$1" == "list-units" ]]; then
|
||||||
|
printf 'gost.service loaded active running\n'
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_legacy_gost_installation >/dev/null
|
||||||
|
|
||||||
|
[[ -e "$LEGACY_GOST_BINARY" ]] || fail "cleanup_legacy_gost_installation should preserve unrelated gost binaries"
|
||||||
|
[[ -e "$LEGACY_GOST_SERVICE_FILE_ETC" ]] || fail "cleanup_legacy_gost_installation should preserve unrelated gost service files"
|
||||||
|
[[ "$systemctl_calls" != *"stop gost"* ]] || fail "cleanup_legacy_gost_installation should not stop unrelated gost services"
|
||||||
|
[[ "$systemctl_calls" != *"disable gost"* ]] || fail "cleanup_legacy_gost_installation should not disable unrelated gost services"
|
||||||
|
)
|
||||||
|
|
||||||
test_install_script_accepts_proxy_url_env_without_prompt() (
|
test_install_script_accepts_proxy_url_env_without_prompt() (
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
load_script_without_main "$ROOT_DIR/install.sh"
|
load_script_without_main "$ROOT_DIR/install.sh"
|
||||||
@@ -303,6 +501,11 @@ test_install_script_asks_for_proxy_config
|
|||||||
test_install_script_recomputes_download_url_after_prompt
|
test_install_script_recomputes_download_url_after_prompt
|
||||||
test_update_flux_agent_asks_for_proxy_config
|
test_update_flux_agent_asks_for_proxy_config
|
||||||
test_update_flux_agent_skips_proxy_prompt_when_not_installed
|
test_update_flux_agent_skips_proxy_prompt_when_not_installed
|
||||||
|
test_install_flux_agent_preserves_legacy_gost_when_download_fails
|
||||||
|
test_update_flux_agent_preserves_legacy_gost_when_download_fails
|
||||||
|
test_install_flux_agent_writes_json_safe_config
|
||||||
|
test_cleanup_legacy_gost_installation_removes_service_and_binary
|
||||||
|
test_cleanup_legacy_gost_installation_preserves_unrelated_gost
|
||||||
test_install_script_accepts_proxy_url_env_without_prompt
|
test_install_script_accepts_proxy_url_env_without_prompt
|
||||||
test_panel_install_script_can_disable_proxy
|
test_panel_install_script_can_disable_proxy
|
||||||
test_panel_install_script_recomputes_compose_urls_after_prompt
|
test_panel_install_script_recomputes_compose_urls_after_prompt
|
||||||
|
|||||||
Reference in New Issue
Block a user