Refactor: Rename NanoKVM to BatchuKVM and update server URL

This commit is contained in:
2025-12-09 20:35:38 +09:00
commit 8cf674c9e5
396 changed files with 54380 additions and 0 deletions

View File

@@ -0,0 +1,147 @@
package tailscale
import (
"NanoKVM-Server/utils"
"bufio"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"regexp"
"strings"
)
const (
ScriptPath = "/etc/init.d/S98tailscaled"
ScriptBackupPath = "/kvmapp/system/init.d/S98tailscaled"
)
type Cli struct{}
type TsStatus struct {
BackendState string `json:"BackendState"`
Self struct {
HostName string `json:"HostName"`
TailscaleIPs []string `json:"TailscaleIPs"`
} `json:"Self"`
CurrentTailnet struct {
Name string `json:"Name"`
} `json:"CurrentTailnet"`
}
func NewCli() *Cli {
return &Cli{}
}
func (c *Cli) Start() error {
for _, filePath := range []string{TailscalePath, TailscaledPath} {
if err := utils.EnsurePermission(filePath, 0o100); err != nil {
return err
}
}
commands := []string{
fmt.Sprintf("cp -f %s %s", ScriptBackupPath, ScriptPath),
fmt.Sprintf("%s start", ScriptPath),
}
command := strings.Join(commands, " && ")
return exec.Command("sh", "-c", command).Run()
}
func (c *Cli) Restart() error {
commands := []string{
fmt.Sprintf("cp -f %s %s", ScriptBackupPath, ScriptPath),
fmt.Sprintf("%s restart", ScriptPath),
}
command := strings.Join(commands, " && ")
return exec.Command("sh", "-c", command).Run()
}
func (c *Cli) Stop() error {
command := fmt.Sprintf("%s stop", ScriptPath)
err := exec.Command("sh", "-c", command).Run()
if err != nil {
return err
}
return os.Remove(ScriptPath)
}
func (c *Cli) Up() error {
command := "tailscale up --accept-dns=false"
return exec.Command("sh", "-c", command).Run()
}
func (c *Cli) Down() error {
command := "tailscale down"
return exec.Command("sh", "-c", command).Run()
}
func (c *Cli) Status() (*TsStatus, error) {
command := "tailscale status --json"
cmd := exec.Command("sh", "-c", command)
output, err := cmd.CombinedOutput()
if err != nil {
return nil, err
}
// output is not in standard json format
if outputStr := string(output); !strings.HasPrefix(outputStr, "{") {
index := strings.Index(outputStr, "{")
if index == -1 {
return nil, errors.New("unknown output")
}
output = []byte(outputStr[index:])
}
var status TsStatus
err = json.Unmarshal(output, &status)
if err != nil {
return nil, err
}
return &status, nil
}
func (c *Cli) Login() (string, error) {
command := "tailscale login --accept-dns=false --timeout=10m"
cmd := exec.Command("sh", "-c", command)
stderr, err := cmd.StderrPipe()
if err != nil {
return "", err
}
defer func() {
_ = stderr.Close()
}()
go func() {
_ = cmd.Run()
}()
reader := bufio.NewReader(stderr)
for {
line, err := reader.ReadString('\n')
if err != nil {
return "", err
}
if strings.Contains(line, "https") {
reg := regexp.MustCompile(`\s+`)
url := reg.ReplaceAllString(line, "")
return url, nil
}
}
}
func (c *Cli) Logout() error {
command := "tailscale logout"
return exec.Command("sh", "-c", command).Run()
}

View File

@@ -0,0 +1,118 @@
package tailscale
import (
"NanoKVM-Server/utils"
"fmt"
"io"
"net/http"
"os"
log "github.com/sirupsen/logrus"
)
const (
OriginalURL = "https://pkgs.tailscale.com/stable/tailscale_latest_riscv64.tgz"
Workspace = "/root/.tailscale"
)
func isInstalled() bool {
_, err1 := os.Stat(TailscalePath)
_, err2 := os.Stat(TailscaledPath)
return err1 == nil && err2 == nil
}
func install() error {
_ = os.MkdirAll(Workspace, 0o755)
defer func() {
_ = os.RemoveAll(Workspace)
}()
tarFile := fmt.Sprintf("%s/tailscale_riscv64.tgz", Workspace)
// download
if err := download(tarFile); err != nil {
log.Errorf("failed to download tailscale: %s", err)
return err
}
// decompress
dir, err := utils.UnTarGz(tarFile, Workspace)
if err != nil {
log.Errorf("failed to decompress tailscale: %s", err)
return err
}
// move
tailscalePath := fmt.Sprintf("%s/tailscale", dir)
err = utils.MoveFile(tailscalePath, TailscalePath)
if err != nil {
log.Errorf("failed to move tailscale: %s", err)
return err
}
tailscaledPath := fmt.Sprintf("%s/tailscaled", dir)
err = utils.MoveFile(tailscaledPath, TailscaledPath)
if err != nil {
log.Errorf("failed to move tailscaled: %s", err)
return err
}
log.Debugf("install tailscale successfully")
return nil
}
func download(target string) error {
url, err := getDownloadURL()
if err != nil {
log.Errorf("failed to get Tailscale download url: %s", err)
return err
}
resp, err := http.Get(url)
if err != nil {
log.Errorf("failed to download Tailscale: %s", err)
return err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
out, err := os.Create(target)
if err != nil {
log.Errorf("failed to create file: %s", err)
return err
}
defer func() {
_ = out.Close()
}()
_, err = io.Copy(out, resp.Body)
if err != nil {
log.Errorf("failed to copy response body to file: %s", err)
return err
}
log.Debugf("download Tailscale successfully")
return nil
}
func getDownloadURL() (string, error) {
resp, err := (&http.Client{}).Get(OriginalURL)
if err != nil {
return "", err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusFound {
return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
return resp.Request.URL.String(), nil
}

View File

@@ -0,0 +1,237 @@
package tailscale
import (
"NanoKVM-Server/proto"
"NanoKVM-Server/utils"
"net"
"os"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
type Service struct{}
const (
TailscalePath = "/usr/bin/tailscale"
TailscaledPath = "/usr/sbin/tailscaled"
GoMemLimit int64 = 75
)
var StateMap = map[string]proto.TailscaleState{
"NoState": proto.TailscaleNotRunning,
"Starting": proto.TailscaleNotRunning,
"NeedsLogin": proto.TailscaleNotLogin,
"Running": proto.TailscaleRunning,
"Stopped": proto.TailscaleStopped,
}
func NewService() *Service {
return &Service{}
}
func (s *Service) Install(c *gin.Context) {
var rsp proto.Response
if !isInstalled() {
if err := install(); err != nil {
rsp.ErrRsp(c, -1, "install failed")
return
}
_ = NewCli().Start()
}
rsp.OkRsp(c)
log.Debugf("install tailscale successfully")
}
func (s *Service) Uninstall(c *gin.Context) {
var rsp proto.Response
_ = NewCli().Stop()
_ = utils.DelGoMemLimit()
_ = os.Remove(TailscalePath)
_ = os.Remove(TailscaledPath)
rsp.OkRsp(c)
log.Debugf("uninstall tailscale successfully")
}
func (s *Service) Start(c *gin.Context) {
var rsp proto.Response
err := NewCli().Start()
if err != nil {
rsp.ErrRsp(c, -1, "start failed")
log.Errorf("failed to run tailscale start: %s", err)
return
}
if !utils.IsGoMemLimitExist() {
_ = utils.SetGoMemLimit(GoMemLimit)
}
rsp.OkRsp(c)
log.Debugf("tailscale start successfully")
}
func (s *Service) Restart(c *gin.Context) {
var rsp proto.Response
err := NewCli().Restart()
if err != nil {
rsp.ErrRsp(c, -1, "restart failed")
log.Errorf("failed to run tailscale restart: %s", err)
return
}
rsp.OkRsp(c)
log.Debugf("tailscale restart successfully")
}
func (s *Service) Stop(c *gin.Context) {
var rsp proto.Response
err := NewCli().Stop()
if err != nil {
rsp.ErrRsp(c, -1, "stop failed")
log.Errorf("failed to run tailscale stop: %s", err)
return
}
_ = utils.DelGoMemLimit()
rsp.OkRsp(c)
log.Debugf("tailscale stop successfully")
}
func (s *Service) Up(c *gin.Context) {
var rsp proto.Response
err := NewCli().Up()
if err != nil {
rsp.ErrRsp(c, -1, "tailscale up failed")
log.Errorf("failed to run tailscale up: %s", err)
return
}
rsp.OkRsp(c)
log.Debugf("run tailscale up successfully")
}
func (s *Service) Down(c *gin.Context) {
var rsp proto.Response
err := NewCli().Down()
if err != nil {
rsp.ErrRsp(c, -1, "tailscale down failed")
log.Errorf("failed to run tailscale down: %s", err)
return
}
rsp.OkRsp(c)
log.Debugf("run tailscale down successfully")
}
func (s *Service) Login(c *gin.Context) {
var rsp proto.Response
// check tailscale status
cli := NewCli()
status, err := cli.Status()
if err != nil {
_ = cli.Start()
status, err = cli.Status()
}
if err != nil {
log.Errorf("failed to get tailscale status: %s", err)
rsp.ErrRsp(c, -1, "unknown status")
return
}
if status.BackendState == "Running" {
rsp.OkRspWithData(c, &proto.LoginTailscaleRsp{})
return
}
// get login url
url, err := cli.Login()
if err != nil {
log.Errorf("failed to run tailscale login: %s", err)
rsp.ErrRsp(c, -2, "login failed")
return
}
if !utils.IsGoMemLimitExist() {
_ = utils.SetGoMemLimit(GoMemLimit)
}
rsp.OkRspWithData(c, &proto.LoginTailscaleRsp{
Url: url,
})
log.Debugf("tailscale login url: %s", url)
}
func (s *Service) Logout(c *gin.Context) {
var rsp proto.Response
err := NewCli().Logout()
if err != nil {
rsp.ErrRsp(c, -1, "logout failed")
log.Errorf("failed to run tailscale logout: %s", err)
return
}
rsp.OkRsp(c)
log.Debugf("tailscale logout successfully")
}
func (s *Service) GetStatus(c *gin.Context) {
var rsp proto.Response
if !isInstalled() {
rsp.OkRspWithData(c, &proto.GetTailscaleStatusRsp{
State: proto.TailscaleNotInstall,
})
return
}
status, err := NewCli().Status()
if err != nil {
log.Debugf("failed to get tailscale status: %s", err)
rsp.OkRspWithData(c, &proto.GetTailscaleStatusRsp{
State: proto.TailscaleNotRunning,
})
return
}
state, ok := StateMap[status.BackendState]
if !ok {
log.Errorf("unknown tailscale state: %s", status.BackendState)
rsp.ErrRsp(c, -1, "unknown state")
return
}
ipv4 := ""
for _, tailscaleIp := range status.Self.TailscaleIPs {
ip := net.ParseIP(tailscaleIp)
if ip != nil && ip.To4() != nil {
ipv4 = ip.String()
}
}
data := proto.GetTailscaleStatusRsp{
State: state,
IP: ipv4,
Name: status.Self.HostName,
Account: status.CurrentTailnet.Name,
}
rsp.OkRspWithData(c, &data)
log.Debugf("get tailscale status successfully")
}