From c622abf194fdf2a4e6e9e7fc66570d7036831d18 Mon Sep 17 00:00:00 2001 From: jb Date: Wed, 11 Mar 2026 16:00:26 -0300 Subject: [PATCH] feat: wireguard / vproxy configuration --- envs | 13 +++ go.mod | 1 + go.sum | 2 + internal/tui/cmds.go | 42 ++++++++++ internal/tui/config.go | 47 +++++++++++ internal/tui/docker.go | 33 ++++++++ internal/tui/model.go | 178 ++++++++++++++++++++++++++++++++--------- internal/tui/steps.go | 5 +- internal/tui/update.go | 107 +++++++++++++++++++++++-- internal/tui/view.go | 15 ++++ 10 files changed, 398 insertions(+), 45 deletions(-) create mode 100644 envs diff --git a/envs b/envs new file mode 100644 index 0000000..a101c62 --- /dev/null +++ b/envs @@ -0,0 +1,13 @@ +PRIVKEY=odkqwjdoqwjdoi +VIP=127.0.0.1 +PSK=qowdjoqwijdoqwi +PROXY_EDPS=qodjoqwjdoqwij + +# MTU Opcional +MTU=1380 + +# Especifica o protocolo de transmissao +# Default: UDP -> Manter se possivel, melhor performance e estabilidade. +# Alguns firewalls restritivos podem impedir o trafego UDP. +# Se nao for possivel negociar a abertura com o cliente, tentar usar TCP +PROTO=UDP diff --git a/go.mod b/go.mod index ade2210..8aff128 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( charm.land/bubbles/v2 v2.0.0 charm.land/bubbletea/v2 v2.0.1 charm.land/lipgloss/v2 v2.0.0 + github.com/BurntSushi/toml v1.6.0 ) require ( diff --git a/go.sum b/go.sum index 0ac7faa..8c15a33 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ charm.land/bubbletea/v2 v2.0.1 h1:B8e9zzK7x9JJ+XvHGF4xnYu9Xa0E0y0MyggY6dbaCfQ= charm.land/bubbletea/v2 v2.0.1/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ= charm.land/lipgloss/v2 v2.0.0 h1:sd8N/B3x892oiOjFfBQdXBQp3cAkvjGaU5TvVZC3ivo= charm.land/lipgloss/v2 v2.0.0/go.mod h1:w6SnmsBFBmEFBodiEDurGS/sdUY/u1+v72DqUzc6J14= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-udiff v0.4.0 h1:TKnLPh7IbnizJIBKFWa9mKayRUBQ9Kh1BPCk6w2PnYM= diff --git a/internal/tui/cmds.go b/internal/tui/cmds.go index a3f216c..b002b4a 100644 --- a/internal/tui/cmds.go +++ b/internal/tui/cmds.go @@ -65,6 +65,28 @@ func DownloadImageCmd(username, password string) tea.Cmd { } } +func DownloadWireguardImageCmd(username, password string) tea.Cmd { + return func() tea.Msg { + url := wireguardImageName + + loginOut, err := exec.Command( + "docker", "login", url, + "-u", username, + "-p", password, + ).CombinedOutput() + + if err != nil { + return ImageDownloadFinishedMsg{ + Message: string(loginOut), + Err: fmt.Errorf("falha no login: %w", err), + } + } + + message, err := PullImage(url) + return ImageDownloadFinishedMsg{Message: message, Err: err} + } +} + func CheckImageCmd(image string) tea.Cmd { return func() tea.Msg { cmd := exec.Command("docker", "image", "inspect", image) @@ -77,6 +99,16 @@ func CheckImageCmd(image string) tea.Cmd { } } +func GenerateWireguardConfigFile(cv ConfigValues, path string) tea.Cmd { + return func() tea.Msg { + err := WriteWireguardConfigFile(cv, path) + + return ConfigFileMsg{ + Err: err, + } + } +} + func GenerateConfigFile(cv ConfigValues, path string) tea.Cmd { return func() tea.Msg { err := WriteConfigFile(cv, path) @@ -96,3 +128,13 @@ func RunAppContainer(image, name, filePath, destinationPath string, cv ConfigVal } } } + +func RunWireguardContainer(path string, cv ConfigValues) tea.Cmd { + return func() tea.Msg { + err := RunWireguardDockerContainer(path, cv) + + return DockerRunMsg{ + Err: err, + } + } +} diff --git a/internal/tui/config.go b/internal/tui/config.go index 66e0a92..15d53f4 100644 --- a/internal/tui/config.go +++ b/internal/tui/config.go @@ -30,6 +30,7 @@ func GenerateConfigTOML(cv ConfigValues) (string, error) { // [certificate] sb.WriteString("# Certificate Options\n") sb.WriteString("[certificate]\n") + sb.WriteString(fmt.Sprintf("mapped_dir = %q\n", cv.Cert["cert_dir_path"])) sb.WriteString(fmt.Sprintf("cert_path = %q\n", "/app/certs/"+cv.Cert["cert_name"])) sb.WriteString(fmt.Sprintf("key_path = %q\n", "/app/certs/"+cv.Cert["key_name"])) sb.WriteString(fmt.Sprintf("ca_path = %q\n", "/app/certs/"+cv.Cert["ca_name"])) @@ -52,6 +53,36 @@ func GenerateConfigTOML(cv ConfigValues) (string, error) { return sb.String(), nil } +func GenerateWireguardConfig(cv ConfigValues) (string, error) { + var sb strings.Builder + + // Primary Wireguard Settings + sb.WriteString(fmt.Sprintf("PRIVKEY=%s\n", cv.Wireguard["privkey"])) + sb.WriteString(fmt.Sprintf("VIP=%s\n", cv.Wireguard["vip"])) + sb.WriteString(fmt.Sprintf("PSK=%s\n", cv.Wireguard["psk"])) + sb.WriteString(fmt.Sprintf("PROXY_EDPS=%s\n", cv.Wireguard["proxy_edps"])) + + sb.WriteString("\n# MTU Opcional\n") + if mtu, ok := cv.Wireguard["mtu"]; ok && mtu != "" { + sb.WriteString(fmt.Sprintf("MTU=%s\n", mtu)) + } else { + sb.WriteString("# MTU=1380\n") + } + + sb.WriteString("\n# Especifica o protocolo de transmissao\n") + sb.WriteString("# Default: UDP -> Manter se possivel, melhor performance e estabilidade.\n") + sb.WriteString("# Alguns firewalls restritivos podem impedir o trafego UDP.\n") + sb.WriteString("# Se nao for possivel negociar a abertura com o cliente, tentar usar TCP\n") + + proto := cv.Wireguard["proto"] + if proto == "" { + proto = "UDP" + } + sb.WriteString(fmt.Sprintf("PROTO=%s\n", proto)) + + return sb.String(), nil +} + func WriteConfigFile(cv ConfigValues, path string) error { // Validate numeric fields before writing numericFields := map[string]string{ @@ -73,3 +104,19 @@ func WriteConfigFile(cv ConfigValues, path string) error { return os.WriteFile(path, []byte(content), 0644) } + +func WriteWireguardConfigFile(cv ConfigValues, path string) error { + // Validate numeric fields before writing + if mtu, ok := cv.Wireguard["mtu"]; ok && mtu != "" { + if _, err := strconv.Atoi(mtu); err != nil { + return fmt.Errorf("o campo MTU deve ser um número, recebido: %q", mtu) + } + } + + content, err := GenerateWireguardConfig(cv) + if err != nil { + return err + } + + return os.WriteFile(path, []byte(content), 0644) +} diff --git a/internal/tui/docker.go b/internal/tui/docker.go index e8a1515..df5137d 100644 --- a/internal/tui/docker.go +++ b/internal/tui/docker.go @@ -45,6 +45,39 @@ func PushFileToContainer(container, filePath, destinationPath string) bool { return err == nil } +func RunWireguardDockerContainer(envFilePath string, cv ConfigValues) error { + containerName := "vproxy" + + removeExistingContainer(containerName) + + absPath, err := filepath.Abs(envFilePath) + if err != nil { + return fmt.Errorf("erro ao resolver caminho absoluto: %w", err) + } + + cmd := exec.Command( + "docker", "run", "-it", "-d", + "--name", containerName, + "--network", "app-dono_app", + "--restart", "unless-stopped", + "--cap-add=NET_ADMIN", + "--device", "/dev/net/tun:/dev/net/tun", + "--log-opt", "max-size=5m", + "--log-opt", "max-file=1", + "--env-file", absPath, + wireguardImageName, + ) + + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("docker run falhou: %w\noutput: %s", err, string(out)) + } + + time.Sleep(2 * time.Second) + containerID := strings.TrimSpace(string(out)) + return verifyContainerRunning(containerID) +} + func RunAppClienteContainer(image, containerName, configPath, configDestinationPath string, cv ConfigValues) error { removeExistingContainer(containerName) diff --git a/internal/tui/model.go b/internal/tui/model.go index d2a1cb7..bd5da45 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -1,8 +1,14 @@ package tui import ( + "fmt" + "os" + "path/filepath" + "strconv" + "charm.land/bubbles/v2/spinner" tea "charm.land/bubbletea/v2" + "github.com/BurntSushi/toml" ) type Model struct { @@ -23,11 +29,12 @@ type Model struct { isPublicIP bool - loginForm FormStep - serverForm FormStep - dbForm FormStep - certForm FormStep - configValues ConfigValues + loginForm FormStep + wireguardForm FormStep + serverForm FormStep + dbForm FormStep + certForm FormStep + configValues ConfigValues finishedFile bool configFileError error @@ -43,13 +50,71 @@ type DockerLoginData struct { } type ConfigValues struct { - Login map[string]string - Server map[string]string - Database map[string]string - Cert map[string]string + Login map[string]string + Wireguard map[string]string + Server map[string]string + Database map[string]string + Cert map[string]string +} + +type AppConfig struct { + Server struct { + Port int64 `toml:"port"` + Timeout int64 `toml:"timeout_seconds"` + Environment string `toml:"environment"` + } `toml:"server"` + Database struct { + Type string `toml:"type"` + URL string `toml:"url"` + MaxConns int64 `toml:"max_conns"` + MinConns int64 `toml:"min_conns"` + } `toml:"database"` + Certificates struct { + DirPath string `toml:"mapped_dir"` + CertName string `toml:"cert_path"` + KeyName string `toml:"key_path"` + CAName string `toml:"ca_path"` + ServerName string `toml:"server_name"` + } `toml:"certificate"` +} + +func loadConfig() AppConfig { + var config AppConfig + + config.Server.Port = 8081 + config.Server.Timeout = 30 + config.Server.Environment = "production" + + config.Database.Type = "postgres" + config.Database.URL = "postgres://usuario:senha@banco:5432/app_dono_db" + config.Database.MaxConns = 10 + config.Database.MinConns = 2 + + config.Certificates.DirPath = "/caminho/para/diretorio" + config.Certificates.CertName = "certificado.crt" + config.Certificates.KeyName = "chave.key" + config.Certificates.CAName = "chaveCA.crt" + config.Certificates.ServerName = "client" + + _, err := os.Stat("config.toml") + if err == nil { + if _, err := toml.DecodeFile("config.toml", &config); err != nil { + fmt.Printf("Error loading config: %v\n", err) + } + } + + if err == nil { + config.Certificates.CertName = filepath.Base(config.Certificates.CertName) + config.Certificates.KeyName = filepath.Base(config.Certificates.KeyName) + config.Certificates.CAName = filepath.Base(config.Certificates.CAName) + } + + return config } func InitialModel() Model { + cfg := loadConfig() + s := spinner.New() s.Spinner = spinner.Dot s.Style = SpinnerStyle @@ -70,12 +135,53 @@ func InitialModel() Model { Type: FieldTypePassword, }, }), + wireguardForm: NewFormStep("Configurações vproxy", []FormField{ + { + Id: "privkey", + Label: "Chave Privada", + Placeholder: "", + Type: FieldTypeText, + }, + { + Id: "vip", + Label: "IP Virtual", + Default: "127.0.0.1", + Placeholder: "127.0.0.1", + Type: FieldTypeText, + }, + { + Id: "psk", + Label: "Pre-Shared Key", + Placeholder: "", + Type: FieldTypeText, + }, + { + Id: "proxy_edps", + Label: "Proxy EDPS", + Placeholder: "22:127.0.0.1:22", + Type: FieldTypeText, + }, + { + Id: "mtu", + Label: "MTU", + Default: "1380", + Placeholder: "1380", + Type: FieldTypeNumber, + }, + { + Id: "proto", + Label: "Protocolo", + Default: "UDP", + Type: FieldTypeSelect, + Options: []string{"UDP", "TCP"}, + }, + }), serverForm: NewFormStep("Servidor", []FormField{ { Id: "port", Label: "Porta", Placeholder: "8081", - Default: "8081", + Default: strconv.FormatInt(cfg.Server.Port, 10), Type: FieldTypeNumber, CharLimit: 4, }, @@ -83,14 +189,14 @@ func InitialModel() Model { Id: "timeout", Label: "Timeout (Segundos)", Placeholder: "30", - Default: "30", + Default: strconv.FormatInt(cfg.Server.Timeout, 10), Type: FieldTypeNumber, CharLimit: 3, }, { Id: "environment", Label: "Ambiente", - Default: "production", + Default: cfg.Server.Environment, Type: FieldTypeSelect, Options: []string{"development", "production"}, }, @@ -99,7 +205,7 @@ func InitialModel() Model { { Id: "database_type", Label: "Tipo do Banco", - Default: "postgres", + Default: cfg.Database.Type, Type: FieldTypeSelect, Options: []string{"postgres", "oracle"}, }, @@ -107,59 +213,55 @@ func InitialModel() Model { Id: "database_url", Label: "URL de acesso", Placeholder: "postgres://usuario:senha@banco:5432/app_dono_db", - Default: "postgres://usuario:senha@banco:5432/app_dono_db", + Default: cfg.Database.URL, Type: FieldTypeText, }, { Id: "max_conns", Label: "Conexões ativas (máximo)", Placeholder: "10", - Default: "10", + Default: strconv.FormatInt(cfg.Database.MaxConns, 10), Type: FieldTypeNumber, }, { Id: "min_conns", Label: "Conexões ativas (mínimo)", Placeholder: "2", - Default: "2", + Default: strconv.FormatInt(cfg.Database.MinConns, 10), Type: FieldTypeNumber, }, }), certForm: NewFormStep("Certificado", []FormField{ { Id: "cert_dir_path", - Label: "Caminho para o diretório dos certificados (será montado no container)", + Label: "Caminho para o diretório dos certificados", Placeholder: "/caminho/para/diretorio", - Default: "/caminho/para/diretorio", + Default: cfg.Certificates.DirPath, Type: FieldTypeText, }, { - Id: "cert_name", - Label: "Nome do arquivo do certificado", - Placeholder: "certificado.crt", - Default: "certificado.crt", - Type: FieldTypeText, + Id: "cert_name", + Label: "Nome do arquivo do certificado", + Default: cfg.Certificates.CertName, + Type: FieldTypeText, }, { - Id: "key_name", - Label: "Nome do arquivo da chave", - Placeholder: "chave.key", - Default: "chave.key", - Type: FieldTypeText, + Id: "key_name", + Label: "Nome do arquivo da chave", + Default: cfg.Certificates.KeyName, + Type: FieldTypeText, }, { - Id: "ca_name", - Label: "Nome do arquivo da autoridade certificadora", - Placeholder: "chaveCA.crt", - Default: "chaveCA.crt", - Type: FieldTypeText, + Id: "ca_name", + Label: "Nome do arquivo da autoridade certificadora", + Default: cfg.Certificates.CAName, + Type: FieldTypeText, }, { - Id: "server_name", - Label: "Nome do servidor", - Placeholder: "client", - Default: "client", - Type: FieldTypeText, + Id: "server_name", + Label: "Nome do servidor", + Default: cfg.Certificates.ServerName, + Type: FieldTypeText, }, }), spinner: s, diff --git a/internal/tui/steps.go b/internal/tui/steps.go index fa6ea51..32427e0 100644 --- a/internal/tui/steps.go +++ b/internal/tui/steps.go @@ -13,7 +13,10 @@ const ( // IP Stuff StepIPQuestion - StepInstallWireguard + StepWireguardConfig + StepGenerateWireguardFile + StepDownloadWireguard + StepRunWireguard // Docker Config StepServerConfig diff --git a/internal/tui/update.go b/internal/tui/update.go index cae3fc1..23e780b 100644 --- a/internal/tui/update.go +++ b/internal/tui/update.go @@ -9,8 +9,10 @@ import ( ) const ( - imageName = "hub.davinti.com.br:443/app-dono/app-cliente:latest" - configPath = "config.toml" + imageName = "hub.davinti.com.br:443/app-dono/app-cliente:latest" + wireguardImageName = "hub.davinti.com.br:443/davinti-vproxy:latest" + configPath = "config.toml" + wireguardConfigPath = "envs" ) func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { @@ -49,10 +51,32 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd case StepDownloadImage: return m.updateDownloadImage(msg) + + // Ip & Wireguard case StepIPQuestion: return m.updateIPQuestion(msg) - //case StepInstallWireguard - // return m.updateInstallWireguard(msg) + case StepWireguardConfig: + done, cmd := m.wireguardForm.Update(msg) + + if done { + m.configValues.Wireguard = m.wireguardForm.Values() + + m.downloadDone = false + m.downloadMessage = "" + m.downloadError = nil + m.currentStep = StepGenerateWireguardFile + + return m, GenerateWireguardConfigFile(m.configValues, wireguardConfigPath) + } + + return m, cmd + case StepGenerateWireguardFile: + return m.updateGenerateWireguardFile(msg) + case StepDownloadWireguard: + return m.updateDownloadWireguard(msg) + case StepRunWireguard: + return m.updateRunWireguardDocker(msg) + case StepServerConfig: done, cmd := m.serverForm.Update(msg) @@ -78,6 +102,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.configValues.Cert = m.certForm.Values() m.currentStep = StepGenerateFile + m.finishedFile = false + m.configFileError = nil + return m, GenerateConfigFile(m.configValues, configPath) } @@ -169,8 +196,73 @@ func (m Model) updateIPQuestion(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } - m.currentStep = StepInstallWireguard - return m, nil + m.currentStep = StepWireguardConfig + } + } + + return m, nil +} + +func (m Model) updateDownloadWireguard(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case ImageDownloadFinishedMsg: + m.downloadDone = true + m.downloadMessage = msg.Message + m.downloadError = msg.Err + + if m.downloadError == nil { + m.currentStep = StepRunWireguard + + return m, RunWireguardContainer(wireguardConfigPath, m.configValues) + } + + case tea.KeyPressMsg: + if m.downloadDone && m.downloadError == nil { + m.currentStep = StepRunWireguard + } else if m.downloadDone { + return m, tea.Quit + } + } + + return m, nil +} + +func (m Model) updateGenerateWireguardFile(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case ConfigFileMsg: + m.finishedFile = true + m.configFileError = msg.Err + + if msg.Err == nil { + m.currentStep = StepDownloadWireguard + + return m, DownloadWireguardImageCmd(m.configValues.Login["user"], m.configValues.Login["password"]) + } + + case tea.KeyPressMsg: + if m.finishedFile && m.configFileError != nil { + return m, tea.Quit + } else if m.finishedFile && m.configFileError == nil { + m.currentStep = StepDownloadWireguard + + return m, DownloadWireguardImageCmd(m.configValues.Login["user"], m.configValues.Login["password"]) + } + } + + return m, nil +} + +func (m Model) updateRunWireguardDocker(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case DockerRunMsg: + m.finishedDockerRun = true + m.dockerRunError = msg.Err + + case tea.KeyPressMsg: + if m.finishedDockerRun && m.dockerRunError != nil { + return m, tea.Quit + } else if m.finishedDockerRun && m.dockerRunError == nil { + m.currentStep = StepServerConfig } } @@ -189,6 +281,9 @@ func (m Model) updateGenerateFile(msg tea.Msg) (tea.Model, tea.Cmd) { } else if m.finishedFile && m.configFileError == nil { m.currentStep = StepRunDocker + m.finishedDockerRun = false + m.dockerRunError = nil + return m, RunAppContainer( imageName, "app-dono-cliente", diff --git a/internal/tui/view.go b/internal/tui/view.go index 604c462..4673838 100644 --- a/internal/tui/view.go +++ b/internal/tui/view.go @@ -36,6 +36,21 @@ func (m Model) View() tea.View { // IP Stuff case StepIPQuestion: body = m.viewIPQuestion() + case StepWireguardConfig: + body = m.wireguardForm.View() + helpMsg = formMsg + case StepGenerateWireguardFile: + body = m.viewGenerateFile() + if m.finishedFile && m.configFileError != nil { + helpMsg = anyKeyOutMsg + } + case StepDownloadWireguard: + body = m.viewDownloadImage() + case StepRunWireguard: + body = m.viewDockerRun() + if m.finishedDockerRun && m.dockerRunError != nil { + helpMsg = anyKeyOutMsg + } // App Config Stuff case StepServerConfig: