Browse Source

feat: wireguard / vproxy configuration

master
jb 2 weeks ago
parent
commit
c622abf194
10 changed files with 398 additions and 45 deletions
  1. +13
    -0
      envs
  2. +1
    -0
      go.mod
  3. +2
    -0
      go.sum
  4. +42
    -0
      internal/tui/cmds.go
  5. +47
    -0
      internal/tui/config.go
  6. +33
    -0
      internal/tui/docker.go
  7. +140
    -38
      internal/tui/model.go
  8. +4
    -1
      internal/tui/steps.go
  9. +101
    -6
      internal/tui/update.go
  10. +15
    -0
      internal/tui/view.go

+ 13
- 0
envs View File

@ -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

+ 1
- 0
go.mod View File

@ -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 (


+ 2
- 0
go.sum View File

@ -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=


+ 42
- 0
internal/tui/cmds.go View File

@ -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,
}
}
}

+ 47
- 0
internal/tui/config.go View File

@ -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)
}

+ 33
- 0
internal/tui/docker.go View File

@ -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)


+ 140
- 38
internal/tui/model.go View File

@ -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,


+ 4
- 1
internal/tui/steps.go View File

@ -13,7 +13,10 @@ const (
// IP Stuff
StepIPQuestion
StepInstallWireguard
StepWireguardConfig
StepGenerateWireguardFile
StepDownloadWireguard
StepRunWireguard
// Docker Config
StepServerConfig


+ 101
- 6
internal/tui/update.go View File

@ -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",


+ 15
- 0
internal/tui/view.go View File

@ -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:


Loading…
Cancel
Save