diff options
| author | sergei <sergei@em-sysadmin.xyz> | 2026-04-14 06:23:55 +0400 |
|---|---|---|
| committer | sergei <sergei@em-sysadmin.xyz> | 2026-04-14 06:23:55 +0400 |
| commit | 3d51aa455006903345f554a2dd90034993796114 (patch) | |
| tree | 62a7be2faf047f5eb7886feebc3b815556f03d7f /internal/control/dns.go | |
| download | vpnem-3d51aa455006903345f554a2dd90034993796114.tar.gz vpnem-3d51aa455006903345f554a2dd90034993796114.tar.bz2 vpnem-3d51aa455006903345f554a2dd90034993796114.zip | |
- Multi-protocol VPS nodes (VLESS-REALITY + Hysteria2 + SOCKS5)
- Smart load balancing via recommendation API
- Windows/Linux client (Go + Wails + sing-box)
- Server API with RealIP detection and connection tracking
- Auto-deployment via vpnui control plane
- Silent Windows installer with UAC elevation
- Load-based server recommendation (no sticky sessions)
- Best Server one-click connection workflow
Diffstat (limited to 'internal/control/dns.go')
| -rw-r--r-- | internal/control/dns.go | 163 |
1 files changed, 163 insertions, 0 deletions
diff --git a/internal/control/dns.go b/internal/control/dns.go new file mode 100644 index 0000000..f841d91 --- /dev/null +++ b/internal/control/dns.go @@ -0,0 +1,163 @@ +package control + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + "time" +) + +const porkbunAPIHost = "https://api.porkbun.com/api/json/v3" + +var porkbunAPIHostOverride string + +type DNSProvider interface { + EnsureRandomARecord(ctx context.Context, zone, prefix, ip string, ttl int) (string, error) + DeleteARecord(ctx context.Context, zone, name string) error +} + +type PorkbunClient struct { + APIKey string + SecretAPIKey string + HTTPClient *http.Client +} + +type porkbunResponse struct { + Status string `json:"status"` + Message string `json:"message"` + Records []map[string]any `json:"records"` + ID string `json:"id"` +} + +func (c PorkbunClient) EnsureRandomARecord(ctx context.Context, zone, prefix, ip string, ttl int) (string, error) { + if err := c.validate(); err != nil { + return "", err + } + if ttl == 0 { + ttl = 600 + } + + for range 10 { + name := randomSubdomain(prefix) + records, err := c.retrieveRecordsByNameType(ctx, zone, "A", name) + if err != nil { + return "", err + } + if len(records) > 0 { + continue + } + if err := c.createRecord(ctx, zone, name, "A", ip, ttl); err != nil { + return "", err + } + return name + "." + zone, nil + } + + return "", errors.New("failed to allocate unique subdomain") +} + +func (c PorkbunClient) DeleteARecord(ctx context.Context, zone, name string) error { + if err := c.validate(); err != nil { + return err + } + return c.deleteByNameType(ctx, zone, "A", name) +} + +func (c PorkbunClient) validate() error { + if strings.TrimSpace(c.APIKey) == "" || strings.TrimSpace(c.SecretAPIKey) == "" { + return errors.New("porkbun api keys are not configured") + } + return nil +} + +func (c PorkbunClient) createRecord(ctx context.Context, zone, name, recordType, content string, ttl int) error { + payload := map[string]string{ + "secretapikey": c.SecretAPIKey, + "apikey": c.APIKey, + "name": name, + "type": recordType, + "content": content, + "ttl": fmt.Sprintf("%d", ttl), + } + _, err := c.post(ctx, "/dns/create/"+zone, payload) + return err +} + +func (c PorkbunClient) deleteByNameType(ctx context.Context, zone, recordType, name string) error { + payload := map[string]string{ + "secretapikey": c.SecretAPIKey, + "apikey": c.APIKey, + } + _, err := c.post(ctx, "/dns/deleteByNameType/"+zone+"/"+recordType+"/"+name, payload) + return err +} + +func (c PorkbunClient) retrieveRecordsByNameType(ctx context.Context, zone, recordType, name string) ([]map[string]any, error) { + payload := map[string]string{ + "secretapikey": c.SecretAPIKey, + "apikey": c.APIKey, + } + resp, err := c.post(ctx, "/dns/retrieveByNameType/"+zone+"/"+recordType+"/"+name, payload) + if err != nil { + return nil, err + } + return resp.Records, nil +} + +func (c PorkbunClient) post(ctx context.Context, path string, payload map[string]string) (*porkbunResponse, error) { + data, err := json.Marshal(payload) + if err != nil { + return nil, err + } + + client := c.HTTPClient + if client == nil { + client = &http.Client{Timeout: 15 * time.Second} + } + + baseURL := porkbunAPIHost + if porkbunAPIHostOverride != "" { + baseURL = porkbunAPIHostOverride + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+path, bytes.NewReader(data)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var out porkbunResponse + if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("porkbun http %d: %s", resp.StatusCode, out.Message) + } + if strings.ToUpper(out.Status) != "SUCCESS" { + return nil, fmt.Errorf("porkbun api error: %s", out.Message) + } + return &out, nil +} + +func randomSubdomain(prefix string) string { + if prefix == "" { + prefix = "vpn" + } + var buf [4]byte + if _, err := rand.Read(buf[:]); err != nil { + now := time.Now().UTC().UnixNano() + return fmt.Sprintf("%s-%x", prefix, now) + } + return prefix + "-" + hex.EncodeToString(buf[:]) +} |
