initial commit

This commit is contained in:
2026-04-07 17:41:25 +02:00
commit 1ed9bdfa55
45 changed files with 4712 additions and 0 deletions

View File

@@ -0,0 +1,140 @@
// Package wireguard generates WireGuard configuration files.
package wireguard
import (
"bytes"
"fmt"
"os"
"strings"
"text/template"
"time"
"protonvpn-wg-confgen/internal/api"
"protonvpn-wg-confgen/internal/config"
"protonvpn-wg-confgen/internal/constants"
)
// wireguardConfigTemplate is the template for generating WireGuard configuration
const wireguardConfigTemplate = `[Interface]
PrivateKey = {{.PrivateKey}}
{{.AddressLine}}
DNS = {{.DNS}}
[Peer]
PublicKey = {{.PublicKey}}
AllowedIPs = {{.AllowedIPs}}
Endpoint = {{.Endpoint}}:{{.Port}}
`
// configData holds the data for the WireGuard config template
type configData struct {
PrivateKey string
AddressLine string
DNS string
PublicKey string
AllowedIPs string
Endpoint string
Port int
}
// ConfigGenerator generates WireGuard configuration files
type ConfigGenerator struct {
config *config.Config
template *template.Template
}
// NewConfigGenerator creates a new configuration generator
func NewConfigGenerator(cfg *config.Config) *ConfigGenerator {
tmpl := template.Must(template.New("wireguard").Parse(wireguardConfigTemplate))
return &ConfigGenerator{
config: cfg,
template: tmpl,
}
}
// Generate creates a WireGuard configuration file
func (g *ConfigGenerator) Generate(server *api.LogicalServer, physicalServer *api.PhysicalServer, privateKey string) error {
content, err := g.buildConfig(server, physicalServer, privateKey)
if err != nil {
return err
}
if err := os.WriteFile(g.config.OutputFile, []byte(content), 0o600); err != nil {
return fmt.Errorf("failed to write config file: %w", err)
}
return nil
}
func (g *ConfigGenerator) buildConfig(server *api.LogicalServer, physicalServer *api.PhysicalServer, privateKey string) (string, error) {
// Build metadata header
metadata := g.buildMetadata(server, physicalServer)
data := configData{
PrivateKey: privateKey,
AddressLine: g.buildAddressLine(),
DNS: strings.Join(g.config.DNSServers, ", "),
PublicKey: physicalServer.X25519PublicKey,
AllowedIPs: strings.Join(g.config.AllowedIPs, ", "),
Endpoint: physicalServer.EntryIP,
Port: constants.WireGuardPort,
}
var buf bytes.Buffer
if err := g.template.Execute(&buf, data); err != nil {
return "", fmt.Errorf("failed to execute template: %w", err)
}
return metadata + buf.String(), nil
}
func (g *ConfigGenerator) buildAddressLine() string {
if g.config.EnableIPv6 {
return fmt.Sprintf("Address = %s, %s", constants.WireGuardIPv4, constants.WireGuardIPv6)
}
return fmt.Sprintf("Address = %s", constants.WireGuardIPv4)
}
func (g *ConfigGenerator) buildMetadata(server *api.LogicalServer, physicalServer *api.PhysicalServer) string {
var metadata strings.Builder
metadata.WriteString("# ProtonVPN WireGuard Configuration\n")
metadata.WriteString(fmt.Sprintf("# Generated: %s\n", time.Now().Format("2006-01-02 15:04:05 MST")))
if g.config.DeviceName != "" {
metadata.WriteString(fmt.Sprintf("# Device: %s\n", g.config.DeviceName))
}
metadata.WriteString("#\n")
metadata.WriteString("# Server Information:\n")
metadata.WriteString(fmt.Sprintf("# - Name: %s\n", server.Name))
metadata.WriteString(fmt.Sprintf("# - Country: %s\n", server.ExitCountry))
metadata.WriteString(fmt.Sprintf("# - City: %s\n", server.City))
metadata.WriteString(fmt.Sprintf("# - Tier: %s\n", api.GetTierName(server.Tier)))
metadata.WriteString(fmt.Sprintf("# - Load: %d%%\n", server.Load))
metadata.WriteString(fmt.Sprintf("# - Score: %.2f\n", server.Score))
// Add features if any
features := api.GetFeatureNames(server.Features)
if len(features) > 0 {
metadata.WriteString(fmt.Sprintf("# - Features: %s\n", strings.Join(features, ", ")))
}
// Add physical server info
metadata.WriteString("#\n")
metadata.WriteString("# Physical Server:\n")
metadata.WriteString(fmt.Sprintf("# - ID: %s\n", physicalServer.ID))
metadata.WriteString(fmt.Sprintf("# - Entry IP: %s\n", physicalServer.EntryIP))
if physicalServer.ExitIP != physicalServer.EntryIP {
metadata.WriteString(fmt.Sprintf("# - Exit IP: %s\n", physicalServer.ExitIP))
}
// Add secure core routing info if applicable
if server.EntryCountry != server.ExitCountry && server.EntryCountry != "" {
metadata.WriteString("#\n")
metadata.WriteString(fmt.Sprintf("# Secure Core Routing: %s → %s\n",
server.EntryCountry, server.ExitCountry))
}
metadata.WriteString("#\n\n")
return metadata.String()
}

View File

@@ -0,0 +1,119 @@
package wireguard
import (
"strings"
"testing"
"protonvpn-wg-confgen/internal/api"
"protonvpn-wg-confgen/internal/config"
)
func TestConfigGeneration(t *testing.T) {
cfg := &config.Config{
DNSServers: []string{"10.2.0.1"},
AllowedIPs: []string{"0.0.0.0/0"},
OutputFile: "test.conf",
}
generator := NewConfigGenerator(cfg)
server := &api.LogicalServer{
Name: "Test-Server",
}
physicalServer := &api.PhysicalServer{
EntryIP: "192.168.1.1",
X25519PublicKey: "testPublicKey123=",
}
privateKey := "testPrivateKey456="
result, err := generator.buildConfig(server, physicalServer, privateKey)
if err != nil {
t.Fatalf("buildConfig failed: %v", err)
}
// Check that config starts with comment header
if !strings.HasPrefix(result, "# ProtonVPN WireGuard Configuration") {
t.Errorf("Expected config to start with header comment, got:\n%s", result[:100])
}
// Check that metadata is present
if !strings.Contains(result, "# - Name: Test-Server") {
t.Errorf("Expected server name in metadata")
}
// Check for proper WireGuard sections
if !strings.Contains(result, "[Interface]") {
t.Error("Expected [Interface] section")
}
if !strings.Contains(result, "[Peer]") {
t.Error("Expected [Peer] section")
}
// Verify key content
expectedContent := []string{
"PrivateKey = testPrivateKey456=",
"Address = 10.2.0.2/32",
"DNS = 10.2.0.1",
"PublicKey = testPublicKey123=",
"AllowedIPs = 0.0.0.0/0",
"Endpoint = 192.168.1.1:51820",
}
for _, expected := range expectedContent {
if !strings.Contains(result, expected) {
t.Errorf("Expected config to contain '%s'\nGot:\n%s", expected, result)
}
}
// Check section order: [Interface] should come before [Peer]
interfaceIdx := strings.Index(result, "[Interface]")
peerIdx := strings.Index(result, "[Peer]")
if interfaceIdx >= peerIdx {
t.Error("[Interface] section should come before [Peer] section")
}
}
func TestConfigGenerationWithIPv6(t *testing.T) {
cfg := &config.Config{
DNSServers: []string{"10.2.0.1", "2a07:b944::2:1"},
AllowedIPs: []string{"0.0.0.0/0", "::/0"},
OutputFile: "test.conf",
EnableIPv6: true,
}
generator := NewConfigGenerator(cfg)
server := &api.LogicalServer{
Name: "Test-Server",
}
physicalServer := &api.PhysicalServer{
EntryIP: "192.168.1.1",
X25519PublicKey: "testPublicKey123=",
}
privateKey := "testPrivateKey456="
result, err := generator.buildConfig(server, physicalServer, privateKey)
if err != nil {
t.Fatalf("buildConfig failed: %v", err)
}
// Check that IPv6 address is included
if !strings.Contains(result, "Address = 10.2.0.2/32, 2a07:b944::2:2/128") {
t.Errorf("Expected IPv6 address in config, got:\n%s", result)
}
// Check DNS servers
if !strings.Contains(result, "DNS = 10.2.0.1, 2a07:b944::2:1") {
t.Errorf("Expected both DNS servers in config, got:\n%s", result)
}
// Check AllowedIPs
if !strings.Contains(result, "AllowedIPs = 0.0.0.0/0, ::/0") {
t.Errorf("Expected both IPv4 and IPv6 in AllowedIPs, got:\n%s", result)
}
}