mirror of https://github.com/go-gost/gost.git
Merge 3f82cdb2f2 into 340ba32ef0
commit
365b3bd820
|
|
@ -36,7 +36,7 @@ func (p *program) Init(env svc.Environment) error {
|
||||||
parser.Init(parser.Args{
|
parser.Init(parser.Args{
|
||||||
CfgFile: cfgFile,
|
CfgFile: cfgFile,
|
||||||
Services: services,
|
Services: services,
|
||||||
Nodes: nodes,
|
Nodes: expandSSHNodes(nodes),
|
||||||
Debug: debug,
|
Debug: debug,
|
||||||
Trace: trace,
|
Trace: trace,
|
||||||
ApiAddr: apiAddr,
|
ApiAddr: apiAddr,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,193 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// sshHostEntry holds the resolved settings for a single SSH host alias.
|
||||||
|
type sshHostEntry struct {
|
||||||
|
hostname string // HostName directive
|
||||||
|
port string // Port directive
|
||||||
|
user string // User directive
|
||||||
|
identityFile string // first IdentityFile directive
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSSHConfigLine splits an SSH config line into (key, value).
|
||||||
|
// Handles both "Key Value" and "Key=Value" (with optional spaces around =).
|
||||||
|
func parseSSHConfigLine(line string) (key, value string, ok bool) {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
// Find first whitespace or '='
|
||||||
|
i := 0
|
||||||
|
for i < len(line) && line[i] != ' ' && line[i] != '\t' && line[i] != '=' {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
if i == len(line) {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
key = line[:i]
|
||||||
|
rest := strings.TrimLeft(line[i:], " \t=")
|
||||||
|
// Strip trailing inline comment (must be preceded by whitespace)
|
||||||
|
if idx := strings.Index(rest, " #"); idx >= 0 {
|
||||||
|
rest = strings.TrimSpace(rest[:idx])
|
||||||
|
}
|
||||||
|
value = strings.Trim(rest, `"'`)
|
||||||
|
return key, value, value != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// readSSHConfig parses ~/.ssh/config and returns a map of host alias/pattern
|
||||||
|
// to resolved entry. Only exact-match patterns (no wildcards) are indexed so
|
||||||
|
// they can be looked up directly; wildcard patterns are skipped for now.
|
||||||
|
func readSSHConfig() map[string]*sshHostEntry {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
path := filepath.Join(home, ".ssh", "config")
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
entries := make(map[string]*sshHostEntry)
|
||||||
|
var current *sshHostEntry
|
||||||
|
var currentPatterns []string
|
||||||
|
|
||||||
|
commit := func() {
|
||||||
|
if current == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, pat := range currentPatterns {
|
||||||
|
// Skip wildcard patterns — they can't be used for exact lookup.
|
||||||
|
if strings.ContainsAny(pat, "*?") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := entries[pat]; !exists {
|
||||||
|
entries[pat] = current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
for scanner.Scan() {
|
||||||
|
key, value, ok := parseSSHConfigLine(scanner.Text())
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch strings.ToLower(key) {
|
||||||
|
case "host":
|
||||||
|
commit()
|
||||||
|
currentPatterns = strings.Fields(value)
|
||||||
|
current = &sshHostEntry{}
|
||||||
|
case "hostname":
|
||||||
|
if current != nil && current.hostname == "" {
|
||||||
|
current.hostname = value
|
||||||
|
}
|
||||||
|
case "port":
|
||||||
|
if current != nil && current.port == "" {
|
||||||
|
current.port = value
|
||||||
|
}
|
||||||
|
case "user":
|
||||||
|
if current != nil && current.user == "" {
|
||||||
|
current.user = value
|
||||||
|
}
|
||||||
|
case "identityfile":
|
||||||
|
if current != nil && current.identityFile == "" {
|
||||||
|
if strings.HasPrefix(value, "~/") {
|
||||||
|
home, _ := os.UserHomeDir()
|
||||||
|
value = filepath.Join(home, value[2:])
|
||||||
|
}
|
||||||
|
current.identityFile = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
commit()
|
||||||
|
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
// expandSSHNode rewrites a single node URL string using ~/.ssh/config when the
|
||||||
|
// scheme is "ssh" and the host matches a Host entry. Fields already present in
|
||||||
|
// the URL are never overridden.
|
||||||
|
func expandSSHNode(raw string, cfg map[string]*sshHostEntry) string {
|
||||||
|
if cfg == nil || !strings.HasPrefix(raw, "ssh://") {
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := url.Parse(raw)
|
||||||
|
if err != nil || u.Scheme != "ssh" {
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
|
host := u.Hostname()
|
||||||
|
port := u.Port()
|
||||||
|
|
||||||
|
entry, ok := cfg[host]
|
||||||
|
if !ok {
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve hostname.
|
||||||
|
newHost := host
|
||||||
|
if entry.hostname != "" {
|
||||||
|
newHost = entry.hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve port: only substitute when the URL carries no explicit port or
|
||||||
|
// carries the SSH default (22) and the config specifies a different one.
|
||||||
|
newPort := port
|
||||||
|
if newPort == "" || newPort == "22" {
|
||||||
|
if entry.port != "" && entry.port != "22" {
|
||||||
|
newPort = entry.port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if newPort != "" && newPort != "22" {
|
||||||
|
u.Host = net.JoinHostPort(newHost, newPort)
|
||||||
|
} else {
|
||||||
|
u.Host = newHost
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply user if not already present in the URL.
|
||||||
|
if entry.user != "" && (u.User == nil || u.User.Username() == "") {
|
||||||
|
var pw string
|
||||||
|
if u.User != nil {
|
||||||
|
pw, _ = u.User.Password()
|
||||||
|
}
|
||||||
|
if pw != "" {
|
||||||
|
u.User = url.UserPassword(entry.user, pw)
|
||||||
|
} else {
|
||||||
|
u.User = url.User(entry.user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply identity file as the "key" query parameter when not already set.
|
||||||
|
if entry.identityFile != "" {
|
||||||
|
q := u.Query()
|
||||||
|
if q.Get("privateKeyFile") == "" {
|
||||||
|
q.Set("privateKeyFile", entry.identityFile)
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// expandSSHNodes applies expandSSHNode to every element of the node list and
|
||||||
|
// returns the result as a new slice. The original slice is not modified.
|
||||||
|
func expandSSHNodes(nodes []string) []string {
|
||||||
|
cfg := readSSHConfig()
|
||||||
|
result := make([]string, len(nodes))
|
||||||
|
for i, n := range nodes {
|
||||||
|
result[i] = expandSSHNode(n, cfg)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,226 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeSSHConfig(t *testing.T, content string) (cleanup func()) {
|
||||||
|
t.Helper()
|
||||||
|
dir := t.TempDir()
|
||||||
|
sshDir := filepath.Join(dir, ".ssh")
|
||||||
|
if err := os.Mkdir(sshDir, 0700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(sshDir, "config"), []byte(content), 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
old := os.Getenv("HOME")
|
||||||
|
os.Setenv("HOME", dir)
|
||||||
|
return func() { os.Setenv("HOME", old) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseSSHConfigLine(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
line, wantKey, wantVal string
|
||||||
|
wantOk bool
|
||||||
|
}{
|
||||||
|
{" HostName example.com", "HostName", "example.com", true},
|
||||||
|
{"HostName=example.com", "HostName", "example.com", true},
|
||||||
|
{"HostName = example.com", "HostName", "example.com", true},
|
||||||
|
{"# comment", "", "", false},
|
||||||
|
{"", "", "", false},
|
||||||
|
{`IdentityFile "~/.ssh/id_rsa"`, "IdentityFile", "~/.ssh/id_rsa", true},
|
||||||
|
{"Host myalias", "Host", "myalias", true},
|
||||||
|
{"Host alias1 alias2", "Host", "alias1 alias2", true},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
k, v, ok := parseSSHConfigLine(c.line)
|
||||||
|
if ok != c.wantOk || k != c.wantKey || v != c.wantVal {
|
||||||
|
t.Errorf("parseSSHConfigLine(%q) = (%q, %q, %v), want (%q, %q, %v)",
|
||||||
|
c.line, k, v, ok, c.wantKey, c.wantVal, c.wantOk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExpandSSHNode_NoMatch(t *testing.T) {
|
||||||
|
cfg := map[string]*sshHostEntry{
|
||||||
|
"myalias": {hostname: "real.host.com", port: "2222", user: "alice"},
|
||||||
|
}
|
||||||
|
if got := expandSSHNode("http://myalias:8080", cfg); got != "http://myalias:8080" {
|
||||||
|
t.Errorf("non-SSH URL was modified: %q", got)
|
||||||
|
}
|
||||||
|
if got := expandSSHNode("ssh://other:22", cfg); got != "ssh://other:22" {
|
||||||
|
t.Errorf("unknown SSH host was modified: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExpandSSHNode_FullExpansion(t *testing.T) {
|
||||||
|
keyPath := "/home/alice/.ssh/id_ed25519"
|
||||||
|
cfg := map[string]*sshHostEntry{
|
||||||
|
"myalias": {
|
||||||
|
hostname: "real.host.com",
|
||||||
|
port: "2222",
|
||||||
|
user: "alice",
|
||||||
|
identityFile: keyPath,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := expandSSHNode("ssh://myalias", cfg)
|
||||||
|
u, err := url.Parse(got)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse result URL: %v", err)
|
||||||
|
}
|
||||||
|
if u.Hostname() != "real.host.com" {
|
||||||
|
t.Errorf("hostname: got %q, want %q", u.Hostname(), "real.host.com")
|
||||||
|
}
|
||||||
|
if u.Port() != "2222" {
|
||||||
|
t.Errorf("port: got %q, want %q", u.Port(), "2222")
|
||||||
|
}
|
||||||
|
if u.User.Username() != "alice" {
|
||||||
|
t.Errorf("user: got %q, want %q", u.User.Username(), "alice")
|
||||||
|
}
|
||||||
|
if q := u.Query().Get("privateKeyFile"); q != keyPath {
|
||||||
|
t.Errorf("privateKeyFile param: got %q, want %q", q, keyPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExpandSSHNode_PreserveExisting(t *testing.T) {
|
||||||
|
cfg := map[string]*sshHostEntry{
|
||||||
|
"myalias": {
|
||||||
|
hostname: "real.host.com",
|
||||||
|
port: "2222",
|
||||||
|
user: "alice",
|
||||||
|
identityFile: "/home/alice/.ssh/id_ed25519",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := expandSSHNode("ssh://bob@myalias:22?key=/tmp/other_key", cfg)
|
||||||
|
u, err := url.Parse(got)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse result URL: %v", err)
|
||||||
|
}
|
||||||
|
if u.Hostname() != "real.host.com" {
|
||||||
|
t.Errorf("hostname: got %q, want %q", u.Hostname(), "real.host.com")
|
||||||
|
}
|
||||||
|
// Port 22 is default; config has 2222 → should be substituted.
|
||||||
|
if u.Port() != "2222" {
|
||||||
|
t.Errorf("port: got %q, want %q", u.Port(), "2222")
|
||||||
|
}
|
||||||
|
// Existing user in URL must be preserved.
|
||||||
|
if u.User.Username() != "bob" {
|
||||||
|
t.Errorf("user: got %q, want %q", u.User.Username(), "bob")
|
||||||
|
}
|
||||||
|
// Existing TLS key param must be preserved unchanged.
|
||||||
|
if q := u.Query().Get("key"); q != "/tmp/other_key" {
|
||||||
|
t.Errorf("key param: got %q, want %q", q, "/tmp/other_key")
|
||||||
|
}
|
||||||
|
// privateKeyFile from config must be added (key is TLS, not SSH).
|
||||||
|
if q := u.Query().Get("privateKeyFile"); q != "/home/alice/.ssh/id_ed25519" {
|
||||||
|
t.Errorf("privateKeyFile param: got %q, want %q", q, "/home/alice/.ssh/id_ed25519")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExpandSSHNode_PrivateKeyFileInURL_Preserved(t *testing.T) {
|
||||||
|
cfg := map[string]*sshHostEntry{
|
||||||
|
"srv": {hostname: "1.2.3.4", identityFile: "/home/user/.ssh/id_rsa"},
|
||||||
|
}
|
||||||
|
got := expandSSHNode("ssh://srv?privateKeyFile=/custom/key", cfg)
|
||||||
|
u, _ := url.Parse(got)
|
||||||
|
// privateKeyFile already set → identityFile from config should NOT override.
|
||||||
|
if q := u.Query().Get("privateKeyFile"); q != "/custom/key" {
|
||||||
|
t.Errorf("privateKeyFile param: got %q, want %q", q, "/custom/key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadSSHConfig(t *testing.T) {
|
||||||
|
cleanup := writeSSHConfig(t, `
|
||||||
|
# global defaults
|
||||||
|
Host *
|
||||||
|
ServerAliveInterval 60
|
||||||
|
|
||||||
|
Host bastion
|
||||||
|
HostName bastion.example.com
|
||||||
|
Port 2222
|
||||||
|
User deploy
|
||||||
|
IdentityFile ~/.ssh/bastion_key
|
||||||
|
|
||||||
|
Host dev
|
||||||
|
HostName 10.0.0.5
|
||||||
|
User dev
|
||||||
|
`)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
cfg := readSSHConfig()
|
||||||
|
if cfg == nil {
|
||||||
|
t.Fatal("readSSHConfig returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
e, ok := cfg["bastion"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected 'bastion' entry")
|
||||||
|
}
|
||||||
|
if e.hostname != "bastion.example.com" {
|
||||||
|
t.Errorf("hostname: got %q", e.hostname)
|
||||||
|
}
|
||||||
|
if e.port != "2222" {
|
||||||
|
t.Errorf("port: got %q", e.port)
|
||||||
|
}
|
||||||
|
if e.user != "deploy" {
|
||||||
|
t.Errorf("user: got %q", e.user)
|
||||||
|
}
|
||||||
|
if filepath.Base(e.identityFile) != "bastion_key" {
|
||||||
|
t.Errorf("identityFile: got %q", e.identityFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
dev, ok := cfg["dev"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected 'dev' entry")
|
||||||
|
}
|
||||||
|
if dev.hostname != "10.0.0.5" {
|
||||||
|
t.Errorf("dev hostname: got %q", dev.hostname)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wildcard Host * must not be indexed by exact key.
|
||||||
|
if _, ok := cfg["*"]; ok {
|
||||||
|
t.Error("wildcard Host * should not appear in entries map")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExpandSSHNodes_Integration(t *testing.T) {
|
||||||
|
cleanup := writeSSHConfig(t, `
|
||||||
|
Host jump
|
||||||
|
HostName jump.corp.example.com
|
||||||
|
Port 2200
|
||||||
|
User ops
|
||||||
|
IdentityFile ~/.ssh/jump_key
|
||||||
|
`)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
nodes := []string{
|
||||||
|
"ssh://jump",
|
||||||
|
"http://proxy:8080",
|
||||||
|
"ssh://realhost:22",
|
||||||
|
}
|
||||||
|
got := expandSSHNodes(nodes)
|
||||||
|
|
||||||
|
u0, _ := url.Parse(got[0])
|
||||||
|
if u0.Hostname() != "jump.corp.example.com" {
|
||||||
|
t.Errorf("[0] hostname: got %q", u0.Hostname())
|
||||||
|
}
|
||||||
|
if u0.Port() != "2200" {
|
||||||
|
t.Errorf("[0] port: got %q", u0.Port())
|
||||||
|
}
|
||||||
|
if u0.User.Username() != "ops" {
|
||||||
|
t.Errorf("[0] user: got %q", u0.User.Username())
|
||||||
|
}
|
||||||
|
if got[1] != "http://proxy:8080" {
|
||||||
|
t.Errorf("[1] non-SSH URL changed: %q", got[1])
|
||||||
|
}
|
||||||
|
// ssh://realhost:22 — not in config, unchanged.
|
||||||
|
if got[2] != "ssh://realhost:22" {
|
||||||
|
t.Errorf("[2] unknown host changed: %q", got[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
var (
|
var (
|
||||||
version = "3.2.6"
|
version = "3.2.7"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,238 @@
|
||||||
|
#Requires -RunAsAdministrator
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Builds gost from source and installs it as a Windows service.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Runs "go build" against the local source tree, copies the resulting binary
|
||||||
|
to a target directory, and registers it as a Windows service using the
|
||||||
|
native Windows Service Control Manager. gost is built with go-svc and
|
||||||
|
runs as a proper Windows service without any wrapper.
|
||||||
|
|
||||||
|
.PARAMETER InstallDir
|
||||||
|
Directory where gost.exe and gost.yml are placed.
|
||||||
|
Default: C:\Program Files\gost
|
||||||
|
|
||||||
|
.PARAMETER ConfigFile
|
||||||
|
Path to an existing gost config file to use. If omitted and no config
|
||||||
|
exists in InstallDir, a minimal placeholder is created.
|
||||||
|
|
||||||
|
.PARAMETER ServiceName
|
||||||
|
Windows service name. Default: gost
|
||||||
|
|
||||||
|
.PARAMETER DisplayName
|
||||||
|
Windows service display name. Default: GOST Tunnel
|
||||||
|
|
||||||
|
.PARAMETER ExtraArgs
|
||||||
|
Additional arguments passed to gost.exe, e.g. "-L :8080 -D".
|
||||||
|
The -C flag pointing to the config file is always added automatically.
|
||||||
|
|
||||||
|
.PARAMETER StartupType
|
||||||
|
Service start type: Automatic, Manual, or Disabled. Default: Automatic
|
||||||
|
|
||||||
|
.PARAMETER Start
|
||||||
|
Start the service immediately after installation.
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
# Build, install with defaults, and start immediately
|
||||||
|
.\install-service.ps1 -Start
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
# Install with a custom config
|
||||||
|
.\install-service.ps1 -ConfigFile C:\etc\gost.yml -Start
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
# Install with inline service definition (no config file)
|
||||||
|
.\install-service.ps1 -ExtraArgs "-L socks5://:1080 -L http://:8080" -Start
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding(SupportsShouldProcess)]
|
||||||
|
param(
|
||||||
|
[string]$InstallDir = "C:\Program Files\gost",
|
||||||
|
[string]$ConfigFile = "",
|
||||||
|
[string]$ServiceName = "gost",
|
||||||
|
[string]$DisplayName = "GOST Tunnel",
|
||||||
|
[string]$ExtraArgs = "",
|
||||||
|
[ValidateSet("Automatic","Manual","Disabled")]
|
||||||
|
[string]$StartupType = "Automatic",
|
||||||
|
[switch]$Start
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
# Directory containing this script == repo root
|
||||||
|
$RepoRoot = $PSScriptRoot
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function Write-Step([string]$msg) { Write-Host "`n==> $msg" -ForegroundColor Cyan }
|
||||||
|
function Write-Ok([string]$msg) { Write-Host " OK $msg" -ForegroundColor Green }
|
||||||
|
function Write-Warn([string]$msg) { Write-Host " WARN $msg" -ForegroundColor Yellow }
|
||||||
|
|
||||||
|
function Build-Binary([string]$destDir) {
|
||||||
|
if (-not (Get-Command go -ErrorAction SilentlyContinue)) {
|
||||||
|
throw "go not found in PATH. Please install Go from https://go.dev/dl/"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Step "Building gost from source ($RepoRoot)..."
|
||||||
|
|
||||||
|
if (-not (Test-Path $destDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $destDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$exeDest = Join-Path $destDir "gost.exe"
|
||||||
|
|
||||||
|
# Embed version: prefer git tag, fall back to version.go
|
||||||
|
$ldflags = "-s -w"
|
||||||
|
$version = $null
|
||||||
|
if (Get-Command git -ErrorAction SilentlyContinue) {
|
||||||
|
$version = git -C $RepoRoot describe --tags --abbrev=0 2>$null
|
||||||
|
}
|
||||||
|
if (-not $version) {
|
||||||
|
$verFile = Join-Path $RepoRoot "cmd\gost\version.go"
|
||||||
|
if (Test-Path $verFile) {
|
||||||
|
$match = Select-String -Path $verFile -Pattern 'version\s*=\s*"([^"]+)"'
|
||||||
|
if ($match) { $version = $match.Matches[0].Groups[1].Value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($version) { $ldflags = "-s -w -X 'main.version=$version'" }
|
||||||
|
|
||||||
|
$goArgs = @("build", "-ldflags", $ldflags, "-o", $exeDest, "./cmd/gost")
|
||||||
|
Write-Host " go $($goArgs -join ' ')" -ForegroundColor DarkGray
|
||||||
|
|
||||||
|
& go @goArgs 2>&1 | ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray }
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "go build failed (exit $LASTEXITCODE)" }
|
||||||
|
|
||||||
|
Write-Ok "Built: $exeDest"
|
||||||
|
return $exeDest
|
||||||
|
}
|
||||||
|
|
||||||
|
function Ensure-Config([string]$installDir, [string]$userConfig) {
|
||||||
|
$dest = Join-Path $installDir "gost.yml"
|
||||||
|
|
||||||
|
if ($userConfig -ne "") {
|
||||||
|
if (-not (Test-Path $userConfig)) {
|
||||||
|
throw "Config file not found: $userConfig"
|
||||||
|
}
|
||||||
|
$resolvedSrc = (Resolve-Path $userConfig).Path
|
||||||
|
$resolvedDest = if (Test-Path $dest) { (Resolve-Path $dest).Path } else { "" }
|
||||||
|
if ($resolvedSrc -ne $resolvedDest) {
|
||||||
|
Copy-Item $userConfig $dest -Force
|
||||||
|
Write-Ok "Config copied from $userConfig"
|
||||||
|
}
|
||||||
|
return $dest
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path $dest) {
|
||||||
|
Write-Ok "Using existing config: $dest"
|
||||||
|
return $dest
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create a minimal placeholder config
|
||||||
|
$placeholder = @"
|
||||||
|
# gost configuration file
|
||||||
|
# Documentation: https://gost.run/
|
||||||
|
#
|
||||||
|
# Example: HTTP proxy on port 8080
|
||||||
|
# services:
|
||||||
|
# - name: http-proxy
|
||||||
|
# addr: ":8080"
|
||||||
|
# handler:
|
||||||
|
# type: http
|
||||||
|
# listener:
|
||||||
|
# type: tcp
|
||||||
|
|
||||||
|
log:
|
||||||
|
level: info
|
||||||
|
format: json
|
||||||
|
"@
|
||||||
|
Set-Content -Path $dest -Value $placeholder -Encoding UTF8
|
||||||
|
Write-Warn "A placeholder config was created at $dest"
|
||||||
|
Write-Warn "Edit it before starting the service, or pass -ExtraArgs with -L/-F flags."
|
||||||
|
return $dest
|
||||||
|
}
|
||||||
|
|
||||||
|
function Register-GostService([string]$exePath, [string]$cfgPath, [string]$extraArgs) {
|
||||||
|
$binPath = "`"$exePath`" -C `"$cfgPath`""
|
||||||
|
if ($extraArgs -ne "") { $binPath += " $extraArgs" }
|
||||||
|
|
||||||
|
$existing = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
Write-Step "Service '$ServiceName' already exists — updating..."
|
||||||
|
if ($existing.Status -eq "Running") {
|
||||||
|
Write-Step "Stopping existing service..."
|
||||||
|
Stop-Service -Name $ServiceName -Force
|
||||||
|
$existing.WaitForStatus("Stopped", [TimeSpan]::FromSeconds(30))
|
||||||
|
}
|
||||||
|
sc.exe config $ServiceName binPath= $binPath | Out-Null
|
||||||
|
$startValue = switch ($StartupType) {
|
||||||
|
"Automatic" { "auto" }
|
||||||
|
"Manual" { "demand" }
|
||||||
|
"Disabled" { "disabled" }
|
||||||
|
}
|
||||||
|
sc.exe config $ServiceName start= $startValue | Out-Null
|
||||||
|
Write-Ok "Service updated."
|
||||||
|
} else {
|
||||||
|
Write-Step "Registering service '$ServiceName'..."
|
||||||
|
$startValue = switch ($StartupType) {
|
||||||
|
"Automatic" { "auto" }
|
||||||
|
"Manual" { "demand" }
|
||||||
|
"Disabled" { "disabled" }
|
||||||
|
}
|
||||||
|
sc.exe create $ServiceName `
|
||||||
|
binPath= $binPath `
|
||||||
|
DisplayName= $DisplayName `
|
||||||
|
start= $startValue | Out-Null
|
||||||
|
|
||||||
|
# Restart on failure: 5 s / 10 s / 30 s, reset counter after 1 day
|
||||||
|
sc.exe failure $ServiceName reset= 86400 actions= restart/5000/restart/10000/restart/30000 | Out-Null
|
||||||
|
Write-Ok "Service registered."
|
||||||
|
}
|
||||||
|
|
||||||
|
sc.exe description $ServiceName "GOST (GO Simple Tunnel) - secure tunnel service" | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " gost Windows Service Installer (build from source)" -ForegroundColor White
|
||||||
|
Write-Host " ===================================================" -ForegroundColor White
|
||||||
|
Write-Host " Repo : $RepoRoot"
|
||||||
|
Write-Host " Install dir: $InstallDir"
|
||||||
|
Write-Host " Service : $ServiceName ($StartupType)"
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# 1. Build binary from source
|
||||||
|
$exePath = Build-Binary $InstallDir
|
||||||
|
|
||||||
|
# 2. Ensure config exists
|
||||||
|
$cfgPath = Ensure-Config $InstallDir $ConfigFile
|
||||||
|
|
||||||
|
# 3. Register Windows service
|
||||||
|
Register-GostService $exePath $cfgPath $ExtraArgs
|
||||||
|
|
||||||
|
# 4. Optionally start
|
||||||
|
if ($Start) {
|
||||||
|
Write-Step "Starting service '$ServiceName'..."
|
||||||
|
Start-Service -Name $ServiceName
|
||||||
|
$svc = Get-Service -Name $ServiceName
|
||||||
|
$svc.WaitForStatus("Running", [TimeSpan]::FromSeconds(15))
|
||||||
|
Write-Ok "Service is running."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " Done!" -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " Useful commands:" -ForegroundColor White
|
||||||
|
Write-Host " Start : Start-Service $ServiceName"
|
||||||
|
Write-Host " Stop : Stop-Service $ServiceName"
|
||||||
|
Write-Host " Status : Get-Service $ServiceName"
|
||||||
|
Write-Host " Logs : Get-EventLog -LogName Application -Source $ServiceName -Newest 20"
|
||||||
|
Write-Host " Uninstall: sc.exe delete $ServiceName"
|
||||||
|
Write-Host ""
|
||||||
Loading…
Reference in New Issue