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