mirror of
https://github.com/nxshock/gonx.git
synced 2024-11-27 17:11:01 +05:00
Initial commit
This commit is contained in:
commit
0b430973c9
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Auto detect text files and perform LF normalization
|
||||||
|
* text=auto
|
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# If you prefer the allow list template instead of the deny list, see community template:
|
||||||
|
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||||
|
#
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 nxshock
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
23
PKGBUILD
Normal file
23
PKGBUILD
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
pkgname=gonx
|
||||||
|
pkgver=0.0.1
|
||||||
|
pkgrel=1
|
||||||
|
pkgdesc='Simple reverse proxy server'
|
||||||
|
arch=('x86_64' 'aarch64')
|
||||||
|
url="https://github.com/nxshock/$pkgname"
|
||||||
|
license=('MIT')
|
||||||
|
makedepends=('go' 'git')
|
||||||
|
source=("$url/$pkgname-$pkgver.tar.gz")
|
||||||
|
sha256sums=('SKIP')
|
||||||
|
backup=("etc/$pkgname.conf")
|
||||||
|
|
||||||
|
build() {
|
||||||
|
cd "$pkgname-$pkgver"
|
||||||
|
go build -o "$pkgname" -ldflags "-linkmode=external -s -w" -buildmode=pie -trimpath -mod=readonly -modcacherw
|
||||||
|
}
|
||||||
|
|
||||||
|
package() {
|
||||||
|
cd "$pkgname-$pkgver"
|
||||||
|
install -Dm755 "$pkgname" "$pkgdir"/usr/bin/$pkgname
|
||||||
|
install -Dm644 "$pkgname.conf" "$pkgdir/etc/$pkgname.conf"
|
||||||
|
install -Dm755 $pkgname.service "$pkgdir"/usr/lib/systemd/system/$pkgname.service
|
||||||
|
}
|
31
README.md
Normal file
31
README.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# gonx
|
||||||
|
|
||||||
|
Simple reverse proxy server.
|
||||||
|
|
||||||
|
## Features:
|
||||||
|
|
||||||
|
* Simple TCP redirection
|
||||||
|
* Simple static file server
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Install package
|
||||||
|
2. Edit config in `/etc/gonx.conf`
|
||||||
|
3. Start application with systemd:
|
||||||
|
|
||||||
|
`systemctl start gonx.service`
|
||||||
|
|
||||||
|
## Config example
|
||||||
|
|
||||||
|
```toml
|
||||||
|
LogLevel = "DEBUG" # Log level (DEBUG, INFO, WARN, ERROR)
|
||||||
|
TlsKeysDir = "/etc/letsencrypt/live" # Path to TLS-certificates generated by Certbot
|
||||||
|
TlsListenAddr = ":443" # TLS listen address
|
||||||
|
HttpListenAddr = ":80" # HTTP listen address
|
||||||
|
AcmeChallengePath = "/var/lib/letsencrypt" # Path for ACME challenge files
|
||||||
|
|
||||||
|
# Map of hostname -> redirect URL
|
||||||
|
[TLS]
|
||||||
|
"git.host.com" = "tcp://127.0.0.1:8001" # TCP redirect
|
||||||
|
"www.host.com" = "file:///srv/http" # simple static file server from `/srv/http`
|
||||||
|
```
|
74
config.go
Normal file
74
config.go
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
// Log level
|
||||||
|
LogLevel slog.Level
|
||||||
|
|
||||||
|
// Path to TLS-certificates generated by Certbot
|
||||||
|
TlsKeysDir string
|
||||||
|
|
||||||
|
// TLS listen address
|
||||||
|
TlsListenAddr string
|
||||||
|
|
||||||
|
// HTTP listen address
|
||||||
|
HttpListenAddr string
|
||||||
|
|
||||||
|
// Map of hostname -> redirect URL
|
||||||
|
TLS map[string]string
|
||||||
|
|
||||||
|
// Acme path
|
||||||
|
AcmeChallengePath string
|
||||||
|
|
||||||
|
// Parsed list of servers
|
||||||
|
proxyRules HostMapping
|
||||||
|
|
||||||
|
// loaded TLS keys
|
||||||
|
tlsConfig *tls.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadConfig(configFilePath string) (*Config, error) {
|
||||||
|
config := new(Config)
|
||||||
|
|
||||||
|
_, err := toml.DecodeFile(configFilePath, &config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
config.proxyRules = make(HostMapping)
|
||||||
|
for inputUrlStr, outputUrlStr := range config.TLS {
|
||||||
|
err = config.proxyRules.Add(inputUrlStr, outputUrlStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) initTls() error {
|
||||||
|
c.tlsConfig = new(tls.Config)
|
||||||
|
|
||||||
|
for hostName := range c.proxyRules {
|
||||||
|
slog.Debug("reading tls key", slog.String("host", hostName))
|
||||||
|
certFilePath := filepath.Join(c.TlsKeysDir, hostName, defaultCertFileName)
|
||||||
|
keyFilePath := filepath.Join(c.TlsKeysDir, hostName, defaultKeyFileName)
|
||||||
|
|
||||||
|
cert, err := tls.LoadX509KeyPair(certFilePath, keyFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read tls files error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.tlsConfig.Certificates = append(c.tlsConfig.Certificates, cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
11
consts.go
Normal file
11
consts.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultConfigPath = "/etc/gonx.conf"
|
||||||
|
defaultTlsKeysPath = "/etc/letsencrypt/live"
|
||||||
|
|
||||||
|
defaultCertFileName = "fullchain.pem"
|
||||||
|
defaultKeyFileName = "privkey.pem"
|
||||||
|
|
||||||
|
defaultAcmeChallengePath = "/.well-known/acme-challenge/"
|
||||||
|
)
|
8
go.mod
Normal file
8
go.mod
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
module gonx
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/BurntSushi/toml v1.3.2
|
||||||
|
github.com/lmittmann/tint v1.0.3
|
||||||
|
)
|
4
go.sum
Normal file
4
go.sum
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||||
|
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||||
|
github.com/lmittmann/tint v1.0.3 h1:W5PHeA2D8bBJVvabNfQD/XW9HPLZK1XoPZH0cq8NouQ=
|
||||||
|
github.com/lmittmann/tint v1.0.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
9
gonx.conf
Normal file
9
gonx.conf
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
LogLevel = "INFO" # Log level (DEBUG, INFO, WARN, ERROR)
|
||||||
|
TlsKeysDir = "/etc/letsencrypt/live" # Path to TLS-certificates generated by Certbot
|
||||||
|
TlsListenAddr = ":443" # TLS listen address
|
||||||
|
HttpListenAddr = ":80" # HTTP listen address
|
||||||
|
AcmeChallengePath = "/var/lib/letsencrypt" # Path for ACME challenge files
|
||||||
|
|
||||||
|
[TLS]
|
||||||
|
# "www.example.com" = "file:/srv/http"
|
||||||
|
# "git.example.com" = "tcp://127.0.0.1:8001"
|
42
gonx.service
Normal file
42
gonx.service
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=gonx service
|
||||||
|
After=network.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/bin/gonx
|
||||||
|
User=http
|
||||||
|
Group=http
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5s
|
||||||
|
|
||||||
|
SecureBits=keep-caps
|
||||||
|
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
||||||
|
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||||
|
DevicePolicy=closed
|
||||||
|
IPAccounting=true
|
||||||
|
LockPersonality=true
|
||||||
|
MemoryDenyWriteExecute=true
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateDevices=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectClock=true
|
||||||
|
ProtectControlGroups=true
|
||||||
|
ProtectControlGroups=true
|
||||||
|
ProtectHome=true
|
||||||
|
ProtectHostname=true
|
||||||
|
ProtectKernelLogs=true
|
||||||
|
ProtectKernelModules=true
|
||||||
|
ProtectKernelTunables=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ReadWritePaths=
|
||||||
|
RemoveIPC=true
|
||||||
|
RestrictNamespaces=true
|
||||||
|
RestrictRealtime=true
|
||||||
|
RestrictSUIDSGID=true
|
||||||
|
SystemCallArchitectures=native
|
||||||
|
UMask=0027
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
34
listener.go
Normal file
34
listener.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Listener implements net.Listener and additional Add(net.Conn) method.
|
||||||
|
type Listener struct {
|
||||||
|
c chan net.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewListener() *Listener {
|
||||||
|
c := make(chan net.Conn)
|
||||||
|
|
||||||
|
return &Listener{c: c}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Listener) Add(conn net.Conn) {
|
||||||
|
m.c <- conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Listener) Accept() (net.Conn, error) {
|
||||||
|
return <-m.c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Listener) Close() error {
|
||||||
|
close(m.c)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Listener) Addr() net.Addr {
|
||||||
|
return nil
|
||||||
|
}
|
84
main.go
Normal file
84
main.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/lmittmann/tint"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
configFilePath := defaultConfigPath
|
||||||
|
if len(os.Args) > 1 {
|
||||||
|
configFilePath = os.Args[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := LoadConfig(configFilePath)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to load config", slog.String("err", err.Error()))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := slog.New(tint.NewHandler(os.Stderr, &tint.Options{
|
||||||
|
Level: config.LogLevel,
|
||||||
|
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
||||||
|
if a.Key == slog.TimeKey && len(groups) == 0 {
|
||||||
|
return slog.Attr{}
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}}))
|
||||||
|
slog.SetDefault(logger)
|
||||||
|
|
||||||
|
err = config.initTls()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("init tls error", slog.String("err", err.Error()))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
slog.Debug("Starting TLS listener", slog.String("addr", config.TlsListenAddr))
|
||||||
|
|
||||||
|
listener, err := tls.Listen("tcp", config.TlsListenAddr, config.tlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to open tls listener", slog.String("err", err.Error()))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
conn, err := listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
slog.Debug("incoming connection failed", slog.String("err", err.Error()))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
slog.Debug("incoming connection", slog.String("RemoteAddr", conn.RemoteAddr().String()))
|
||||||
|
|
||||||
|
go func() { _ = handleTlsConn(conn.(*tls.Conn), config.proxyRules) }()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
slog.Debug("Starting HTTP listener", slog.String("addr", config.HttpListenAddr))
|
||||||
|
|
||||||
|
smux := http.NewServeMux()
|
||||||
|
smux.Handle(defaultAcmeChallengePath, http.StripPrefix(defaultAcmeChallengePath, http.FileServer(http.Dir(config.AcmeChallengePath))))
|
||||||
|
smux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Redirect(w, r, "https://"+r.Host+r.RequestURI, http.StatusMovedPermanently) // TODO: заполнить URL переадресации
|
||||||
|
})
|
||||||
|
httpServer := http.Server{Handler: smux}
|
||||||
|
httpServer.Addr = config.HttpListenAddr
|
||||||
|
err := httpServer.ListenAndServe()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to start HTTP server", slog.String("err", err.Error()))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
c := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||||
|
<-c
|
||||||
|
slog.Debug("Interrupt signal received.")
|
||||||
|
}
|
96
mapping.go
Normal file
96
mapping.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProxyDirection struct {
|
||||||
|
Output *url.URL
|
||||||
|
|
||||||
|
listener *Listener
|
||||||
|
}
|
||||||
|
|
||||||
|
type HostMapping map[string]ProxyDirection // hostName -> rule
|
||||||
|
|
||||||
|
func (h HostMapping) Add(host, outputUrlStr string) error {
|
||||||
|
outputUrl, err := url.Parse(outputUrlStr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pd := ProxyDirection{outputUrl, NewListener()}
|
||||||
|
|
||||||
|
switch outputUrl.Scheme {
|
||||||
|
case "file":
|
||||||
|
server := http.Server{Handler: http.FileServer(http.Dir(outputUrl.Path))}
|
||||||
|
go func() { _ = server.Serve(pd.listener) }()
|
||||||
|
case "tcp":
|
||||||
|
go func(pd ProxyDirection) {
|
||||||
|
for {
|
||||||
|
conn, err := pd.listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
slog.Debug(err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
go func() { _ = handleProxy(conn.(*tls.Conn), pd.Output) }()
|
||||||
|
}
|
||||||
|
}(pd)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown output protocol: %v", outputUrl.Scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
h[host] = pd
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTlsConn(conn *tls.Conn, hosts HostMapping) error {
|
||||||
|
err := conn.Handshake()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("handshake error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hostName := conn.ConnectionState().ServerName
|
||||||
|
proxyDirection, exists := hosts[hostName]
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("requested host not found: %s", hostName)
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyDirection.listener.Add(conn)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleProxy(conn *tls.Conn, outputUrl *url.URL) error {
|
||||||
|
c, err := net.Dial(outputUrl.Scheme, outputUrl.Host)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("dial: %v", err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
wg := new(sync.WaitGroup)
|
||||||
|
wg.Add(2)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
_, _ = io.Copy(conn, c)
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
_, _ = io.Copy(c, conn)
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user