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
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
- uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db
- uses: opentofu/setup-opentofu@a1320f892987e89d278cc92dc5adc984fb93aca4 # v2.0.2
with:
tofu_version: 1.12.1
tofu_version: 1.12.6
- name: Read platform deployment context
id: platform
env:
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/destroy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ jobs:
service_account: ${{ vars.GCP_APPLY_SERVICE_ACCOUNT }}
- uses: opentofu/setup-opentofu@a1320f892987e89d278cc92dc5adc984fb93aca4 # v2.0.2
with:
tofu_version: 1.12.1
tofu_version: 1.12.6
- name: Destroy platform only
run: make destroy ENV="${{ inputs.environment }}" STACK=platform CONFIRM_PROJECT_ID="${{ inputs.confirm_project_id }}"

3 changes: 1 addition & 2 deletions .github/workflows/infra-apply.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ jobs:
service_account: ${{ vars.GCP_APPLY_SERVICE_ACCOUNT }}
- uses: opentofu/setup-opentofu@a1320f892987e89d278cc92dc5adc984fb93aca4 # v2.0.2
with:
tofu_version: 1.12.1
tofu_version: 1.12.6
- name: Regenerate plan and apply
run: make infra-apply ENV="${{ inputs.environment }}" STACK="${{ inputs.stack }}"

2 changes: 1 addition & 1 deletion .github/workflows/infra-plan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ jobs:
- uses: opentofu/setup-opentofu@a1320f892987e89d278cc92dc5adc984fb93aca4 # v2.0.2
if: steps.selected.outputs.selected == 'true'
with:
tofu_version: 1.12.1
tofu_version: 1.12.6
- name: Validate infrastructure without credentials
if: steps.selected.outputs.selected == 'true'
run: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: opentofu/setup-opentofu@a1320f892987e89d278cc92dc5adc984fb93aca4 # v2.0.2
with:
tofu_version: 1.12.1
tofu_version: 1.12.6
- name: Install validation tools
run: |
sudo apt-get update
Expand Down
68 changes: 40 additions & 28 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# Makefile for Proxy Builder (Sing-box)
# Sing-box 原生模式代理服务管理

.PHONY: all uuid short-id password reality-key secrets-init secrets-import-production secrets-publish subscription-url user-add user-enable user-disable user-rotate github-reset github-configure github-clean-production-legacy github-audit bootstrap validate infra-plan infra-apply deploy destroy help
.PHONY: all uuid short-id password reality-key secrets-init secrets-import-production secrets-publish subscription-url user-add user-enable user-disable user-rotate user-protocol-enable user-protocol-disable github-reset github-configure github-clean-production-legacy github-audit bootstrap validate infra-plan infra-apply deploy destroy help

# Accept short operator-facing aliases while keeping canonical names everywhere
# that forms part of a path, Terraform state, or GitHub Environment identity.
CANONICAL_ENV = $(if $(filter dev,$(ENV)),development,$(if $(filter prod,$(ENV)),production,$(ENV)))

help:
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
Expand All @@ -13,23 +17,25 @@ help:
@echo " make short-id - Generate a random 8-character hex ID"
@echo " make password - Generate a random secure password"
@echo " make reality-key - Generate REALITY key pair (uses Docker)"
@echo " make secrets-init ENV=... USER=..."
@echo " ENV accepts dev|development or prod|production"
@echo " make secrets-init ENV=dev|prod USER=..."
@echo " make secrets-import-production"
@echo " make user-add|user-enable|user-disable|user-rotate ENV=... USER=..."
@echo " make secrets-publish ENV=..."
@echo " make subscription-url ENV=... USER=... [FORMAT=base64|clash]"
@echo " make user-add|user-enable|user-disable|user-rotate ENV=dev|prod USER=..."
@echo " make user-protocol-enable|user-protocol-disable ENV=dev|prod USER=... PROTOCOL=vless|hysteria2"
@echo " make secrets-publish ENV=dev|prod"
@echo " make subscription-url ENV=dev|prod USER=... [FORMAT=base64|clash]"
@echo ""
@echo "🏗️ OpenTofu Platform:"
@echo " make bootstrap ENV=development|production"
@echo " make github-reset ENV=development CONFIRM=owner/repo:development"
@echo " make github-configure ENV=development|production"
@echo " make bootstrap ENV=dev|prod"
@echo " make github-reset ENV=dev CONFIRM=owner/repo:development"
@echo " make github-configure ENV=dev|prod"
@echo " make github-clean-production-legacy CONFIRM=owner/repo:production"
@echo " make github-audit ENV=development|production"
@echo " make github-audit ENV=dev|prod"
@echo " make validate"
@echo " make infra-plan ENV=... [STACK=bootstrap|platform]"
@echo " make infra-apply ENV=... [STACK=bootstrap|platform]"
@echo " make deploy ENV=... [GIT_REF=<ref>]"
@echo " make destroy ENV=... STACK=platform"
@echo " make infra-plan ENV=dev|prod [STACK=bootstrap|platform]"
@echo " make infra-apply ENV=dev|prod [STACK=bootstrap|platform]"
@echo " make deploy ENV=dev|prod [GIT_REF=<ref>]"
@echo " make destroy ENV=dev|prod STACK=platform"


# ============================================================
Expand All @@ -50,59 +56,65 @@ reality-key:
@docker run --rm ghcr.io/sagernet/sing-box generate reality-keypair

secrets-init:
@ENV="$(ENV)" USER="$(USER)" ./scripts/secrets/init.sh
@ENV="$(CANONICAL_ENV)" USER="$(USER)" ./scripts/secrets/init.sh

secrets-import-production:
@./scripts/secrets/import-production.sh

user-add:
@ENV="$(ENV)" USER="$(USER)" ./scripts/secrets/manage-user.sh add
@ENV="$(CANONICAL_ENV)" USER="$(USER)" ./scripts/secrets/manage-user.sh add

user-enable:
@ENV="$(ENV)" USER="$(USER)" ./scripts/secrets/manage-user.sh enable
@ENV="$(CANONICAL_ENV)" USER="$(USER)" ./scripts/secrets/manage-user.sh enable

user-disable:
@ENV="$(ENV)" USER="$(USER)" ./scripts/secrets/manage-user.sh disable
@ENV="$(CANONICAL_ENV)" USER="$(USER)" ./scripts/secrets/manage-user.sh disable

user-rotate:
@ENV="$(ENV)" USER="$(USER)" ./scripts/secrets/manage-user.sh rotate
@ENV="$(CANONICAL_ENV)" USER="$(USER)" ./scripts/secrets/manage-user.sh rotate

user-protocol-enable:
@ENV="$(CANONICAL_ENV)" USER="$(USER)" PROTOCOL="$(PROTOCOL)" ./scripts/secrets/manage-user.sh protocol-enable

user-protocol-disable:
@ENV="$(CANONICAL_ENV)" USER="$(USER)" PROTOCOL="$(PROTOCOL)" ./scripts/secrets/manage-user.sh protocol-disable

secrets-publish:
@./scripts/github/publish-secrets.sh --environment "$(ENV)" --secret-dir ".secrets/$(ENV)"
@./scripts/github/publish-secrets.sh --environment "$(CANONICAL_ENV)" --secret-dir ".secrets/$(CANONICAL_ENV)"

subscription-url:
@ENV="$(ENV)" USER="$(USER)" FORMAT="$(or $(FORMAT),base64)" ./scripts/secrets/subscription-url.sh
@ENV="$(CANONICAL_ENV)" USER="$(USER)" FORMAT="$(or $(FORMAT),base64)" ./scripts/secrets/subscription-url.sh

# ============================================================
# OpenTofu Platform
# ============================================================

bootstrap:
@ENV="$(ENV)" ./scripts/infra/bootstrap-state.sh
@ENV="$(CANONICAL_ENV)" ./scripts/infra/bootstrap-state.sh

github-configure:
@./scripts/github/configure.sh --environment "$(ENV)"
@./scripts/github/configure.sh --environment "$(CANONICAL_ENV)"

github-clean-production-legacy:
@./scripts/github/clear-legacy-production-secrets.sh --confirm "$(CONFIRM)"

github-reset:
@./scripts/github/reset-environment.sh --environment "$(ENV)" --confirm "$(CONFIRM)"
@./scripts/github/reset-environment.sh --environment "$(CANONICAL_ENV)" --confirm "$(CONFIRM)"

github-audit:
@./scripts/github/audit.sh --environment "$(ENV)"
@./scripts/github/audit.sh --environment "$(CANONICAL_ENV)"

validate:
@./scripts/validate.sh

infra-plan:
@ENV="$(ENV)" STACK="$(or $(STACK),platform)" ./scripts/infra/tofu-stack.sh plan
@ENV="$(CANONICAL_ENV)" STACK="$(or $(STACK),platform)" ./scripts/infra/tofu-stack.sh plan

infra-apply:
@ENV="$(ENV)" STACK="$(or $(STACK),platform)" ./scripts/infra/tofu-stack.sh apply
@ENV="$(CANONICAL_ENV)" STACK="$(or $(STACK),platform)" ./scripts/infra/tofu-stack.sh apply

deploy:
@ENV="$(ENV)" GIT_REF="$(GIT_REF)" ./scripts/github/dispatch-deploy.sh
@ENV="$(CANONICAL_ENV)" GIT_REF="$(GIT_REF)" ./scripts/github/dispatch-deploy.sh

destroy:
@ENV="$(ENV)" CONFIRM_PROJECT_ID="$(CONFIRM_PROJECT_ID)" ./scripts/infra/destroy-platform.sh
@ENV="$(CANONICAL_ENV)" CONFIRM_PROJECT_ID="$(CONFIRM_PROJECT_ID)" ./scripts/infra/destroy-platform.sh
18 changes: 15 additions & 3 deletions cmd/proxyctl/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ const (
userMutationEnable
userMutationDisable
userMutationRotate
userMutationEnableProtocol
userMutationDisableProtocol
)

func initEnvironment(args []string) error {
Expand Down Expand Up @@ -113,17 +115,22 @@ func mutateUser(args []string, mutation userMutation) error {
command := map[userMutation]string{
userMutationAdd: "add-user", userMutationEnable: "enable-user",
userMutationDisable: "disable-user", userMutationRotate: "rotate-user",
userMutationEnableProtocol: "enable-user-protocol", userMutationDisableProtocol: "disable-user-protocol",
}[mutation]
flags := flag.NewFlagSet(command, flag.ContinueOnError)
flags.SetOutput(os.Stderr)
usersPath := flags.String("users", "", "users JSON path")
name := flags.String("name", "", "user name")
protocol := flags.String("protocol", "", "vless or hysteria2")
if err := flags.Parse(args); err != nil {
return fmt.Errorf("%s arguments are invalid", command)
}
if *usersPath == "" || *name == "" || flags.NArg() != 0 {
return fmt.Errorf("%s requires --users and --name", command)
}
if (mutation == userMutationEnableProtocol || mutation == userMutationDisableProtocol) && *protocol != "vless" && *protocol != "hysteria2" {
return fmt.Errorf("%s requires --protocol=vless|hysteria2", command)
}
info, err := os.Lstat(*usersPath)
if err != nil || !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
return errors.New("users input must be a regular file")
Expand Down Expand Up @@ -153,21 +160,26 @@ func mutateUser(args []string, mutation userMutation) error {
return err
}
document.Users = append(document.Users, user)
case userMutationEnable, userMutationDisable, userMutationRotate:
case userMutationEnable, userMutationDisable, userMutationRotate, userMutationEnableProtocol, userMutationDisableProtocol:
if index < 0 {
return errors.New("user name does not exist")
}
if mutation == userMutationEnable {
document.Users[index].Enabled = true
} else if mutation == userMutationDisable {
document.Users[index].Enabled = false
} else {
} else if mutation == userMutationRotate {
rotated, err := newUser(document.Users[index].Name)
if err != nil {
return err
}
rotated.Enabled = document.Users[index].Enabled
rotated.Protocols = document.Users[index].Protocols
document.Users[index] = rotated
} else if *protocol == "vless" {
document.Users[index].Protocols.VLESS = mutation == userMutationEnableProtocol
} else {
document.Users[index].Protocols.Hysteria2 = mutation == userMutationEnableProtocol
}
}
if err := document.Validate(); err != nil {
Expand All @@ -193,7 +205,7 @@ func newUser(name string) (contracts.User, error) {
if err != nil {
return contracts.User{}, err
}
return contracts.User{Name: name, Enabled: true, VLESSUUID: uuid, HY2Password: password, SubscriptionToken: token}, nil
return contracts.User{Name: name, Enabled: true, Protocols: contracts.Protocols{VLESS: true, Hysteria2: true}, VLESSUUID: uuid, HY2Password: password, SubscriptionToken: token}, nil
}

func randomEncoded(size int) (string, error) {
Expand Down
21 changes: 20 additions & 1 deletion cmd/proxyctl/environment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func TestInitEnvironmentCreatesValidatedPrivateBundle(t *testing.T) {
}
usersData, _ := os.ReadFile(filepath.Join(directory, "users.json"))
document, err := contracts.ParseUsers(usersData)
if err != nil || len(document.Users) != 1 || document.Users[0].Name != "alice" || !document.Users[0].Enabled {
if err != nil || len(document.Users) != 1 || document.Users[0].Name != "alice" || !document.Users[0].Enabled || !document.Users[0].Protocols.VLESS || !document.Users[0].Protocols.Hysteria2 {
t.Fatalf("unexpected users: %#v, err = %v", document, err)
}
privateKey, _ := readTrimmed(filepath.Join(directory, "reality-private-key"), "Reality private key")
Expand Down Expand Up @@ -152,6 +152,19 @@ func TestUserLifecycleUsesImmediateCredentialRotation(t *testing.T) {
if !readUsersForTest(t, path).Users[1].Enabled {
t.Fatal("enable-user left the user disabled")
}
if err := mutateUser([]string{"--users", path, "--name", "alice", "--protocol", "hysteria2"}, userMutationDisableProtocol); err != nil {
t.Fatal(err)
}
protocols := readUsersForTest(t, path).Users[0].Protocols
if !protocols.VLESS || protocols.Hysteria2 {
t.Fatalf("disable-user-protocol did not preserve VLESS-only access: %#v", protocols)
}
if err := mutateUser([]string{"--users", path, "--name", "alice", "--protocol", "hysteria2"}, userMutationEnableProtocol); err != nil {
t.Fatal(err)
}
if !readUsersForTest(t, path).Users[0].Protocols.Hysteria2 {
t.Fatal("enable-user-protocol did not restore Hysteria2 access")
}
}

func TestUserLifecycleRejectsDisablingLastEnabledUserAndSymlink(t *testing.T) {
Expand All @@ -168,6 +181,12 @@ func TestUserLifecycleRejectsDisablingLastEnabledUserAndSymlink(t *testing.T) {
if string(before) != string(after) {
t.Fatal("failed mutation changed users.json")
}
if err := mutateUser([]string{"--users", path, "--name", "alice", "--protocol", "hysteria2"}, userMutationDisableProtocol); err != nil {
t.Fatal(err)
}
if err := mutateUser([]string{"--users", path, "--name", "alice", "--protocol", "vless"}, userMutationDisableProtocol); err == nil {
t.Fatal("disable-user-protocol accepted disabling the final protocol")
}
symlink := filepath.Join(t.TempDir(), "users.json")
if err := os.Symlink(path, symlink); err != nil {
t.Fatal(err)
Expand Down
4 changes: 4 additions & 0 deletions cmd/proxyctl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ func run(args []string) error {
return mutateUser(args[1:], userMutationDisable)
case "rotate-user":
return mutateUser(args[1:], userMutationRotate)
case "enable-user-protocol":
return mutateUser(args[1:], userMutationEnableProtocol)
case "disable-user-protocol":
return mutateUser(args[1:], userMutationDisableProtocol)
case "validate-subscription":
return validateSubscription(args[1:])
case "render-probe-config":
Expand Down
Loading