Files
kvm/vpn.go
2025-09-25 20:14:21 +08:00

556 lines
13 KiB
Go

package kvm
import (
"bufio"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"syscall"
"time"
)
type TailScaleSettings struct {
State string `json:"state"`
LoginUrl string `json:"loginUrl"`
IP string `json:"ip"`
XEdge bool `json:"xEdge"`
}
func rpcCancelTailScale() error {
_, err := CallVpnCtrlAction("cancel_tailscale", map[string]interface{}{"type": "no_param"})
if err != nil {
return err
}
return nil
}
func rpcLoginTailScale(xEdge bool) (TailScaleSettings, error) {
settings := TailScaleSettings{
State: "connecting",
XEdge: xEdge,
LoginUrl: "",
IP: "",
}
_, err := CallVpnCtrlAction("login_tailscale", map[string]interface{}{"xEdge": xEdge})
if err != nil {
return settings, err
}
for i := 0; i < 15; i++ {
time.Sleep(2 * time.Second)
resp, err := CallVpnCtrlAction("get_tailscale_state", map[string]interface{}{"type": "no_param"})
if err != nil {
return settings, err
}
if resp.Event == "tailscale_state" {
if _, ok := resp.Result["state"]; ok {
settings.State = resp.Result["state"].(string)
}
if _, ok := resp.Result["ip"]; ok {
settings.IP = resp.Result["ip"].(string)
}
if _, ok := resp.Result["loginUrl"]; ok {
settings.LoginUrl = resp.Result["loginUrl"].(string)
}
if _, ok := resp.Result["xEdge"]; ok {
settings.XEdge = resp.Result["xEdge"].(bool)
}
}
switch settings.State {
case "logined":
config.TailScaleAutoStart = true
config.TailScaleXEdge = settings.XEdge
err := SaveConfig()
if err != nil {
vpnLogger.Error().Err(err).Msg("failed to save config")
}
return settings, err
case "connected":
config.TailScaleAutoStart = true
config.TailScaleXEdge = settings.XEdge
err = SaveConfig()
if err != nil {
vpnLogger.Error().Err(err).Msg("failed to save config")
}
return settings, err
case "connecting":
if i >= 10 {
settings.State = "disconnected"
} else {
settings.State = "connecting"
}
case "cancel":
err := rpcLogoutTailScale()
if err != nil {
vpnLogger.Error().Err(err).Msg("failed to logout tailscale")
}
settings.State = "disconnected"
return settings, nil
default:
settings.State = "disconnected"
}
}
return settings, nil
}
func rpcLogoutTailScale() error {
_, err := CallVpnCtrlAction("logout_tailscale", map[string]interface{}{"type": "no_param"})
if err != nil {
return err
}
config.TailScaleAutoStart = false
if err := SaveConfig(); err != nil {
return err
}
return nil
}
func rpcGetTailScaleSettings() (TailScaleSettings, error) {
settings := TailScaleSettings{}
resp, err := CallVpnCtrlAction("get_tailscale_state", map[string]interface{}{"type": "no_param"})
if err != nil {
return settings, err
}
if resp.Event == "tailscale_state" {
if _, ok := resp.Result["state"]; ok {
settings.State = resp.Result["state"].(string)
}
if _, ok := resp.Result["ip"]; ok {
settings.IP = resp.Result["ip"].(string)
}
if _, ok := resp.Result["loginUrl"]; ok {
settings.LoginUrl = resp.Result["loginUrl"].(string)
}
if _, ok := resp.Result["xEdge"]; ok {
settings.XEdge = resp.Result["xEdge"].(bool)
}
}
return settings, nil
}
type ZeroTierSettings struct {
State string `json:"state"`
NetworkID string `json:"networkID"`
IP string `json:"ip"`
}
func rpcLoginZeroTier(networkID string) (ZeroTierSettings, error) {
LoadConfig()
settings := ZeroTierSettings{
State: "connecting",
NetworkID: networkID,
IP: "",
}
resp, err := CallVpnCtrlAction("login_zerotier", map[string]interface{}{
"network_id": networkID,
"config_network_id": config.ZeroTierNetworkID,
})
if err != nil {
return ZeroTierSettings{}, err
}
if resp.Event == "zerotier_state" {
if _, ok := resp.Result["state"]; ok {
settings.State = resp.Result["state"].(string)
}
if _, ok := resp.Result["network_id"]; ok {
settings.NetworkID = resp.Result["network_id"].(string)
}
if _, ok := resp.Result["ip"]; ok {
settings.IP = resp.Result["ip"].(string)
}
}
switch settings.State {
case "closed":
config.ZeroTierAutoStart = false
config.ZeroTierNetworkID = ""
if err := SaveConfig(); err != nil {
vpnLogger.Error().Err(err).Msg("failed to save config")
}
case "connected", "logined":
config.ZeroTierAutoStart = true
config.ZeroTierNetworkID = settings.NetworkID
if err := SaveConfig(); err != nil {
vpnLogger.Error().Err(err).Msg("failed to save config")
}
}
/* disconnected - does not handle */
return settings, nil
}
func rpcLogoutZeroTier(networkID string) error {
_, err := CallVpnCtrlAction("logout_zerotier", map[string]interface{}{
"network_id": networkID,
})
if err != nil {
return err
}
config.ZeroTierAutoStart = false
config.ZeroTierNetworkID = ""
if err := SaveConfig(); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}
return nil
}
func rpcGetZeroTierSettings() (ZeroTierSettings, error) {
LoadConfig()
configNetworkID := fmt.Sprintf("%v", config.ZeroTierNetworkID)
settings := ZeroTierSettings{
State: "disconnected",
NetworkID: configNetworkID,
IP: "",
}
resp, err := CallVpnCtrlAction("get_zerotier_state", map[string]interface{}{
"network_id": configNetworkID,
})
if err != nil {
return settings, err
}
if resp.Event == "zerotier_state" {
if _, ok := resp.Result["state"]; ok {
settings.State = resp.Result["state"].(string)
}
if _, ok := resp.Result["network_id"]; ok {
settings.NetworkID = resp.Result["network_id"].(string)
}
if _, ok := resp.Result["ip"]; ok {
settings.IP = resp.Result["ip"].(string)
}
}
return settings, nil
}
type VpnUpdateDisplayState struct {
TailScaleState string `json:"tailscale_state"`
ZeroTierState string `json:"zerotier_state"`
Error string `json:"error,omitempty"` //no_signal, no_lock, out_of_range
}
func HandleVpnDisplayUpdateMessage(event CtrlResponse) {
waitDisplayUpdate.Lock()
defer waitDisplayUpdate.Unlock()
waitDisplayCtrlClientConnected()
vpnUpdateDisplayState := VpnUpdateDisplayState{}
err := json.Unmarshal(event.Data, &vpnUpdateDisplayState)
if err != nil {
vpnLogger.Warn().Err(err).Msg("Error parsing vpn state json")
return
}
switch vpnUpdateDisplayState.TailScaleState {
case "connected":
updateLabelIfChanged("Network_TailScale_Label", "Connected")
case "logined":
updateLabelIfChanged("Network_TailScale_Label", "Logined")
default:
updateLabelIfChanged("Network_TailScale_Label", "Disconnected")
}
switch vpnUpdateDisplayState.ZeroTierState {
case "connected":
updateLabelIfChanged("Network_ZeroTier_Label", "Connected")
case "logined":
updateLabelIfChanged("Network_ZeroTier_Label", "Logined")
default:
updateLabelIfChanged("Network_ZeroTier_Label", "Disconnected")
}
}
type FrpcStatus struct {
Running bool `json:"running"`
}
var (
frpcTomlPath = "/userdata/frpc/frpc.toml"
frpcLogPath = "/tmp/frpc.log"
)
func frpcRunning() bool {
cmd := exec.Command("pgrep", "-x", "frpc")
return cmd.Run() == nil
}
func rpcGetFrpcLog() (string, error) {
f, err := os.Open(frpcLogPath)
if err != nil {
if os.IsNotExist(err) {
return "", fmt.Errorf("frpc log file not exist")
}
return "", err
}
defer f.Close()
const want = 30
lines := make([]string, 0, want+10)
sc := bufio.NewScanner(f)
for sc.Scan() {
lines = append(lines, sc.Text())
if len(lines) > want {
lines = lines[1:]
}
}
if err := sc.Err(); err != nil {
return "", err
}
var buf []byte
for _, l := range lines {
buf = append(buf, l...)
buf = append(buf, '\n')
}
return string(buf), nil
}
func rpcGetFrpcToml() (string, error) {
return config.FrpcToml, nil
}
func rpcStartFrpc(frpcToml string) error {
if frpcRunning() {
_ = exec.Command("pkill", "-x", "frpc").Run()
}
if frpcToml != "" {
_ = os.MkdirAll(filepath.Dir(frpcTomlPath), 0700)
if err := os.WriteFile(frpcTomlPath, []byte(frpcToml), 0600); err != nil {
return err
}
cmd := exec.Command("frpc", "-c", frpcTomlPath)
cmd.Stdout = nil
cmd.Stderr = nil
logFile, err := os.OpenFile(frpcLogPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return err
}
defer logFile.Close()
cmd.Stdout = logFile
cmd.Stderr = logFile
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
if err := cmd.Start(); err != nil {
return fmt.Errorf("start frpc failed: %w", err)
} else {
config.FrpcAutoStart = true
config.FrpcToml = frpcToml
if err := SaveConfig(); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}
}
} else {
return fmt.Errorf("frpcToml is empty")
}
return nil
}
func rpcStopFrpc() error {
if frpcRunning() {
err := exec.Command("pkill", "-x", "frpc").Run()
if err != nil {
return fmt.Errorf("failed to stop frpc: %w", err)
}
}
config.FrpcAutoStart = false
err := SaveConfig()
if err != nil {
return fmt.Errorf("failed to save config: %w", err)
}
return nil
}
func rpcGetFrpcStatus() (FrpcStatus, error) {
return FrpcStatus{Running: frpcRunning()}, nil
}
type EasytierStatus struct {
Running bool `json:"running"`
}
type EasytierConfig struct {
Name string `json:"name"`
Secret string `json:"secret"`
Node string `json:"node"`
}
var (
easytierLogPath = "/tmp/easytier.log"
)
func easytierRunning() bool {
cmd := exec.Command("pgrep", "-x", "easytier-core")
return cmd.Run() == nil
}
func rpcGetEasyTierLog() (string, error) {
f, err := os.Open(easytierLogPath)
if err != nil {
if os.IsNotExist(err) {
return "", fmt.Errorf("easytier log file not exist")
}
return "", err
}
defer f.Close()
const want = 30
lines := make([]string, 0, want+10)
sc := bufio.NewScanner(f)
for sc.Scan() {
lines = append(lines, sc.Text())
if len(lines) > want {
lines = lines[1:]
}
}
if err := sc.Err(); err != nil {
return "", err
}
var buf []byte
for _, l := range lines {
buf = append(buf, l...)
buf = append(buf, '\n')
}
return string(buf), nil
}
func rpcGetEasyTierNodeInfo() (string, error) {
cmd := exec.Command("easytier-cli", "node")
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to get easytier node info: %w", err)
}
return string(output), nil
}
func rpcGetEasyTierConfig() (EasytierConfig, error) {
return config.EasytierConfig, nil
}
func rpcStartEasyTier(name, secret, node string) error {
if easytierRunning() {
_ = exec.Command("pkill", "-x", "easytier-core").Run()
}
if name == "" || secret == "" || node == "" {
return fmt.Errorf("easytier config is invalid")
}
cmd := exec.Command("easytier-core", "-d", "--network-name", name, "--network-secret", secret, "-p", node)
cmd.Stdout = nil
cmd.Stderr = nil
logFile, err := os.OpenFile(easytierLogPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return fmt.Errorf("failed to open easytier log file: %w", err)
}
defer logFile.Close()
cmd.Stdout = logFile
cmd.Stderr = logFile
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
if err := cmd.Start(); err != nil {
return fmt.Errorf("start easytier failed: %w", err)
} else {
config.EasytierAutoStart = true
config.EasytierConfig = EasytierConfig{
Name: name,
Secret: secret,
Node: node,
}
if err := SaveConfig(); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}
}
return nil
}
func rpcStopEasyTier() error {
if easytierRunning() {
err := exec.Command("pkill", "-x", "easytier-core").Run()
if err != nil {
return fmt.Errorf("failed to stop easytier: %w", err)
}
}
config.EasytierAutoStart = false
err := SaveConfig()
if err != nil {
return fmt.Errorf("failed to save config: %w", err)
}
return nil
}
func rpcGetEasyTierStatus() (EasytierStatus, error) {
return EasytierStatus{Running: easytierRunning()}, nil
}
func initVPN() {
waitVpnCtrlClientConnected()
go func() {
for {
if !networkState.IsOnline() {
vpnLogger.Warn().Msg("waiting for network to be online, will retry in 3 seconds")
time.Sleep(3 * time.Second)
continue
} else {
break
}
}
if config.TailScaleAutoStart {
if _, err := rpcLoginTailScale(config.TailScaleXEdge); err != nil {
vpnLogger.Error().Err(err).Msg("Failed to auto start TailScale")
}
}
if config.ZeroTierAutoStart && config.ZeroTierNetworkID != "" {
if _, err := rpcLoginZeroTier(config.ZeroTierNetworkID); err != nil {
vpnLogger.Error().Err(err).Msg("Failed to auto start ZeroTier")
}
}
if config.FrpcAutoStart && config.FrpcToml != "" {
if err := rpcStartFrpc(config.FrpcToml); err != nil {
vpnLogger.Error().Err(err).Msg("Failed to auto start frpc")
}
}
if config.EasytierAutoStart && config.EasytierConfig.Name != "" && config.EasytierConfig.Secret != "" && config.EasytierConfig.Node != "" {
if err := rpcStartEasyTier(config.EasytierConfig.Name, config.EasytierConfig.Secret, config.EasytierConfig.Node); err != nil {
vpnLogger.Error().Err(err).Msg("Failed to auto start easytier")
}
}
}()
go func() {
for {
var status syscall.WaitStatus
var rusage syscall.Rusage
pid, err := syscall.Wait4(-1, &status, syscall.WNOHANG, &rusage)
if pid <= 0 || err != nil {
time.Sleep(5 * time.Second)
}
}
}()
}