Compare commits

..

1 Commits

Author SHA1 Message Date
tqcq
85cabeefa5 feat: push with docker 2026-03-01 20:07:25 +08:00
9 changed files with 164 additions and 28 deletions

16
.gitea/workflows/ci.yaml Normal file
View File

@@ -0,0 +1,16 @@
name: CI
on:
push:
tags:
- v*
branches:
- master
pull_request:
jobs:
docker-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build the Docker image
run: docker build . --file Dockerfile

1
go.mod generated
View File

@@ -14,6 +14,7 @@ require (
github.com/mattn/go-runewidth v0.0.12 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
github.com/olekukonko/tablewriter v0.0.5
github.com/pires/go-proxyproto v0.6.2 // indirect
github.com/pkg/errors v0.9.1
github.com/reiver/go-oi v1.0.0
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e

2
go.sum generated
View File

@@ -158,6 +158,8 @@ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQ
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pires/go-proxyproto v0.6.2 h1:KAZ7UteSOt6urjme6ZldyFm4wDe/z0ZUP0Yv0Dos0d8=
github.com/pires/go-proxyproto v0.6.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7 h1:+/+DxvQaYifJ+grD4klzrS5y+KJXldn/2YTl5JG+vZ8=
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=

15
main.go
View File

@@ -79,12 +79,17 @@ func main() {
Value: 0,
Usage: "Duration before an inactive connection is timed out (0 to disable)",
},
cli.StringFlag{
Name: "acl-check-cmd",
EnvVar: "SSHPORTAL_ACL_CHECK_CMD",
Usage: "Execute external command to check ACL",
},
cli.StringFlag{
Name: "acl-check-cmd",
EnvVar: "SSHPORTAL_ACL_CHECK_CMD",
Usage: "Execute external command to check ACL",
},
cli.BoolFlag{
Name: "no-proxy-protocol",
EnvVar: "SSHPORTAL_NO_PROXY_PROTOCOL",
Usage: "Disable Proxy Protocol auto-detection (default: enabled)",
},
},
}, {
Name: "healthcheck",
Action: func(c *cli.Context) error { return healthcheck(c.String("addr"), c.Bool("wait"), c.Bool("quiet")) },

View File

@@ -5,21 +5,83 @@ import (
"io"
"io/ioutil"
"log"
"net"
"os"
"path/filepath"
"time"
"github.com/gliderlabs/ssh"
"github.com/pires/go-proxyproto"
"github.com/pkg/errors"
"github.com/sabban/bastion/pkg/logchannel"
gossh "golang.org/x/crypto/ssh"
)
type sessionConfig struct {
Addr string
LogsLocation string
ClientConfig *gossh.ClientConfig
LoggingMode string
Addr string
LogsLocation string
ClientConfig *gossh.ClientConfig
LoggingMode string
ProxyProtocol string
ClientIP net.Addr
}
// dialWithProxyProto dials the target host with optional PROXY protocol header
// When PROXY protocol is enabled, the header is sent before the SSH handshake
func dialWithProxyProto(network, addr string, config *gossh.ClientConfig, proxyProtocol string, clientIP net.Addr) (*gossh.Client, error) {
// If Proxy Protocol is enabled, send the header before SSH handshake
if proxyProtocol == "v1" || proxyProtocol == "v2" {
// Parse target address
targetAddr, err := net.ResolveTCPAddr("tcp", addr)
if err != nil {
return nil, err
}
// Parse source address (client IP) or use localhost if unknown
var srcAddr net.Addr
if clientIP != nil {
srcAddr = clientIP
} else {
srcAddr, _ = net.ResolveTCPAddr("tcp", "127.0.0.1:0")
}
// Create a raw TCP connection
conn, err := net.Dial(network, addr)
if err != nil {
return nil, err
}
// Create a PROXY protocol header
header := proxyproto.HeaderProxyFromAddrs(0, srcAddr, targetAddr)
// Set protocol version if specified
if proxyProtocol == "v1" {
header.Version = 1
} else {
header.Version = 2
}
// Write PROXY protocol header before SSH handshake
_, err = header.WriteTo(conn)
if err != nil {
conn.Close()
return nil, err
}
// Create SSH client connection on top of the TCP connection (with PROXY header already sent)
// The SSH handshake will now continue after the PROXY header
c, chans, reqs, err := gossh.NewClientConn(conn, addr, config)
if err != nil {
conn.Close()
return nil, err
}
// Return a client from the connection
return gossh.NewClient(c, chans, reqs), nil
}
// Without Proxy Protocol - use standard dial
return gossh.Dial(network, addr, config)
}
func multiChannelHandler(conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context, configs []sessionConfig, sessionID uint) error {
@@ -37,7 +99,8 @@ func multiChannelHandler(conn *gossh.ServerConn, newChan gossh.NewChannel, ctx s
for _, config := range configs {
var client *gossh.Client
if lastClient == nil {
client, err = gossh.Dial("tcp", config.Addr, config.ClientConfig)
// First connection - use dialWithProxyProto if enabled
client, err = dialWithProxyProto("tcp", config.Addr, config.ClientConfig, config.ProxyProtocol, config.ClientIP)
} else {
rconn, err := lastClient.Dial("tcp", config.Addr)
if err != nil {
@@ -74,11 +137,12 @@ func multiChannelHandler(conn *gossh.ServerConn, newChan gossh.NewChannel, ctx s
return nil
}
// go through all the hops
// go through all hops
for _, config := range configs {
var client *gossh.Client
if lastClient == nil {
client, err = gossh.Dial("tcp", config.Addr, config.ClientConfig)
// First connection - use dialWithProxyProto if enabled
client, err = dialWithProxyProto("tcp", config.Addr, config.ClientConfig, config.ProxyProtocol, config.ClientIP)
} else {
rconn, err := lastClient.Dial("tcp", config.Addr)
if err != nil {

View File

@@ -728,6 +728,7 @@ GLOBAL OPTIONS:
cli.StringFlag{Name: "key, k", Usage: "`KEY` to use for authentication"},
cli.StringFlag{Name: "hop, o", Usage: "Hop to use for connecting to the server"},
cli.StringFlag{Name: "logging, l", Usage: "Logging mode (disabled, input, everything)"},
cli.StringFlag{Name: "proxy-protocol", Usage: "Proxy protocol version (ssh: default, v1, v2)"},
cli.StringSliceFlag{Name: "group, g", Usage: "Assigns the host to `HOSTGROUPS` (default: \"default\")"},
},
Action: func(c *cli.Context) error {
@@ -775,6 +776,12 @@ GLOBAL OPTIONS:
if c.String("logging") != "" {
host.Logging = c.String("logging")
}
if proxyProtocol := c.String("proxy-protocol"); proxyProtocol != "" || proxyProtocol == "ssh" {
if !dbmodels.ValidProxyProtocol(proxyProtocol) {
return fmt.Errorf("invalid proxy protocol version: %q (must be v1 or v2)", proxyProtocol)
}
host.ProxyProtocol = proxyProtocol
}
// FIXME: check if name already exists
if _, err := govalidator.ValidateStruct(host); err != nil {
@@ -882,7 +889,7 @@ GLOBAL OPTIONS:
}
table := tablewriter.NewWriter(s)
table.SetHeader([]string{"ID", "Name", "URL", "Key", "Groups", "Updated", "Created", "Comment", "Hop", "Logging"})
table.SetHeader([]string{"ID", "Name", "URL", "Key", "Groups", "Updated", "Created", "Comment", "Hop", "Logging", "ProxyProtocol"})
table.SetBorder(false)
table.SetCaption(true, fmt.Sprintf("Total: %d hosts.", len(hosts)))
for _, host := range hosts {
@@ -908,6 +915,11 @@ GLOBAL OPTIONS:
} else {
hop = ""
}
var protocol string = "ssh"
if len(host.ProxyProtocol) != 0 {
protocol = host.ProxyProtocol
}
table.Append([]string{
fmt.Sprintf("%d", host.ID),
host.Name,
@@ -919,6 +931,7 @@ GLOBAL OPTIONS:
host.Comment,
hop,
host.Logging,
protocol,
//FIXME: add some stats about last access time etc
})
}
@@ -951,6 +964,7 @@ GLOBAL OPTIONS:
cli.StringFlag{Name: "key, k", Usage: "Link a `KEY` to use for authentication"},
cli.StringFlag{Name: "hop, o", Usage: "Change the hop to use for connecting to the server"},
cli.StringFlag{Name: "logging, l", Usage: "Logging mode (disabled, input, everything)"},
cli.StringFlag{Name: "proxy-protocol, p", Usage: "Proxy protocol version (ssh, v1, v2)"},
cli.BoolFlag{Name: "unset-hop", Usage: "Remove the hop set for this host"},
cli.StringSliceFlag{Name: "assign-group, g", Usage: "Assign the host to a new `HOSTGROUPS`"},
cli.StringSliceFlag{Name: "unassign-group", Usage: "Unassign the host from a `HOSTGROUPS`"},
@@ -1024,6 +1038,17 @@ GLOBAL OPTIONS:
}
}
// proxy-protocol
if proxyProtocol := c.String("proxy-protocol"); proxyProtocol != "" || proxyProtocol == "ssh" {
if !dbmodels.ValidProxyProtocol(proxyProtocol) {
return fmt.Errorf("invalid proxy protocol version: %q (must be v1 or v2)", proxyProtocol)
}
if err := model.Update("proxy_protocol", proxyProtocol).Error; err != nil {
tx.Rollback()
return err
}
}
// remove the hop
if c.Bool("unset-hop") {
var hopHost dbmodels.Host

View File

@@ -141,6 +141,8 @@ func ChannelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewCh
ClientConfig: clientConfig,
LogsLocation: actx.logsLocation,
LoggingMode: currentHost.Logging,
ProxyProtocol: currentHost.ProxyProtocol,
ClientIP: conn.RemoteAddr(),
}}, sessionConfigs...)
if currentHost.HopID != 0 {
var newHost dbmodels.Host

View File

@@ -58,12 +58,13 @@ type Host struct {
URL string `valid:"optional"`
SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"` // SSHKey used to connect by the client
SSHKeyID uint `gorm:"index"`
HostKey []byte `sql:"size:1000" valid:"optional"`
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
Comment string `valid:"optional"`
Logging string `valid:"optional,host_logging_mode"`
Hop *Host
HopID uint
HostKey []byte `sql:"size:1000" valid:"optional"`
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
Comment string `valid:"optional"`
Logging string `valid:"optional,host_logging_mode"`
Hop *Host
HopID uint
ProxyProtocol string `gorm:"column:proxy_protocol;default:''" valid:"optional"`
}
// UserKey defines a user public key used by sshportal to identify the user
@@ -321,6 +322,16 @@ func (host *Host) ClientConfig(hk gossh.HostKeyCallback) (*gossh.ClientConfig, e
return &config, nil
}
// ValidProxyProtocol returns true if the ProxyProtocol value is valid
func ValidProxyProtocol(value string) bool {
switch value {
case "", "v1", "v2":
return true
default:
return false
}
}
// SSHKey helpers
func SSHKeysPreload(db *gorm.DB) *gorm.DB {

View File

@@ -17,6 +17,7 @@ import (
"moul.io/sshportal/pkg/bastion"
"github.com/gliderlabs/ssh"
"github.com/pires/go-proxyproto"
"github.com/urfave/cli"
gossh "golang.org/x/crypto/ssh"
)
@@ -29,19 +30,21 @@ type serverConfig struct {
debug, demo bool
idleTimeout time.Duration
aclCheckCmd string
proxyProtocol bool
}
func parseServerConfig(c *cli.Context) (*serverConfig, error) {
ret := &serverConfig{
aesKey: c.String("aes-key"),
dbDriver: c.String("db-driver"),
dbURL: c.String("db-conn"),
bindAddr: c.String("bind-address"),
debug: c.Bool("debug"),
demo: c.Bool("demo"),
logsLocation: c.String("logs-location"),
idleTimeout: c.Duration("idle-timeout"),
aclCheckCmd: c.String("acl-check-cmd"),
aesKey: c.String("aes-key"),
dbDriver: c.String("db-driver"),
dbURL: c.String("db-conn"),
bindAddr: c.String("bind-address"),
debug: c.Bool("debug"),
demo: c.Bool("demo"),
logsLocation: c.String("logs-location"),
idleTimeout: c.Duration("idle-timeout"),
aclCheckCmd: c.String("acl-check-cmd"),
proxyProtocol: !c.Bool("no-proxy-protocol"), // Default to true (enabled)
}
switch len(ret.aesKey) {
case 0, 16, 24, 32:
@@ -111,6 +114,13 @@ func server(c *serverConfig) (err error) {
return err
}
// wrap listener with Proxy Protocol support if enabled
// When enabled, listener auto-detects: PROXY v1, PROXY v2, or no PROXY (regular SSH)
if c.proxyProtocol {
ln = &proxyproto.Listener{Listener: ln}
log.Printf("Proxy Protocol auto-detection enabled (accepts v1, v2, or plain SSH)")
}
// configure server
srv := &ssh.Server{
Addr: c.bindAddr,