diff --git a/core/endpoint_defaults_test.go b/core/endpoint_defaults_test.go index 2986b1f..41c76ee 100644 --- a/core/endpoint_defaults_test.go +++ b/core/endpoint_defaults_test.go @@ -269,7 +269,10 @@ func writeStaticKey(t *testing.T) string { if _, err := rand.Read(material); err != nil { t.Fatal(err) } + // Same shape as ServerService.generateOpenVPNStaticKey, comment header and + // all, so this covers what the panel's generate button hands over. var body strings.Builder + body.WriteString("#\n# 2048 bit OpenVPN static key\n#\n") body.WriteString("-----BEGIN OpenVPN Static key V1-----\n") encoded := hex.EncodeToString(material) for offset := 0; offset < len(encoded); offset += 32 { diff --git a/frontend b/frontend index faad006..fa41add 160000 --- a/frontend +++ b/frontend @@ -1 +1 @@ -Subproject commit faad00676a658c10930719f4c866d98510db2329 +Subproject commit fa41add99f48a0b84a214e653c5ac36599feb0f8 diff --git a/service/server.go b/service/server.go index 8db4112..1f3c9da 100644 --- a/service/server.go +++ b/service/server.go @@ -1,7 +1,9 @@ package service import ( + "crypto/rand" "encoding/base64" + "encoding/hex" "os" "runtime" "strconv" @@ -212,6 +214,8 @@ func (s *ServerService) GenKeypair(keyType string, options string) []string { return s.generateRealityKeyPair() case "wireguard": return s.generateWireGuardKey(options) + case "openvpn": + return s.generateOpenVPNStaticKey() } return []string{"Failed to generate keypair"} @@ -233,6 +237,29 @@ func (s *ServerService) generateTLSKeyPair(serverName string) []string { return append(strings.Split(string(privateKeyPem), "\n"), strings.Split(string(publicKeyPem), "\n")...) } +// generateOpenVPNStaticKey produces what `openvpn --genkey secret` writes: 256 +// random bytes as hex between OpenVPN's own markers. tls-auth and tls-crypt +// take one of these rather than a PEM, and both ends of a tunnel have to carry +// the same one. +func (s *ServerService) generateOpenVPNStaticKey() []string { + material := make([]byte, 256) + if _, err := rand.Read(material); err != nil { + return []string{"Failed to generate OpenVPN static key: ", err.Error()} + } + encoded := hex.EncodeToString(material) + + lines := []string{ + "#", + "# 2048 bit OpenVPN static key", + "#", + "-----BEGIN OpenVPN Static key V1-----", + } + for offset := 0; offset < len(encoded); offset += 32 { + lines = append(lines, encoded[offset:offset+32]) + } + return append(lines, "-----END OpenVPN Static key V1-----") +} + func (s *ServerService) generateRealityKeyPair() []string { privateKey, err := wgtypes.GeneratePrivateKey() if err != nil { diff --git a/service/server_keypair_test.go b/service/server_keypair_test.go new file mode 100644 index 0000000..129ba1f --- /dev/null +++ b/service/server_keypair_test.go @@ -0,0 +1,43 @@ +package service + +import ( + "strings" + "testing" +) + +// The static key sing-openvpn accepts is 256 bytes of hex between its own +// markers, with anything before the begin marker ignored. TestOpenVPNStaticKey +// checks the shape; core.TestEndpointInlineTLS proves sing-box takes it. +func TestGenerateOpenVPNStaticKey(t *testing.T) { + s := &ServerService{} + lines := s.GenKeypair("openvpn", "") + + begin, end := -1, -1 + for index, line := range lines { + switch line { + case "-----BEGIN OpenVPN Static key V1-----": + begin = index + case "-----END OpenVPN Static key V1-----": + end = index + } + } + if begin == -1 || end <= begin { + t.Fatalf("markers missing or out of order: %v", lines) + } + + body := strings.Join(lines[begin+1:end], "") + if len(body) != 512 { + t.Errorf("key body is %d hex characters, want 512 (256 bytes)", len(body)) + } + for _, r := range body { + if !strings.ContainsRune("0123456789abcdef", r) { + t.Fatalf("key body is not lowercase hex: %q", body) + } + } + + // Two calls must not produce the same key, or every tunnel built through + // the panel would share one. + if strings.Join(s.GenKeypair("openvpn", ""), "") == strings.Join(lines, "") { + t.Error("two generated keys are identical") + } +}