v0.0.1 ready
continuous-integration/drone/tag Build is failing Details

master
Evan Chen 2021-11-06 11:41:36 +08:00
parent 42bbcc5581
commit e1d84a9070
5 changed files with 270 additions and 519 deletions

32
.drone.yml Normal file
View File

@ -0,0 +1,32 @@
kind: pipeline
name: default
steps:
- name: build
image: golang:1.17.2
commands:
- git tag $DRONE_TAG
- bash release.sh
- echo -n "latest,${DRONE_TAG#v}" > .tags
- name: gitea_release
image: plugins/gitea-release
settings:
api_key:
from_secret: gitea_api_key
base_url: https://kumoly.io
files: dist/*
checksum:
- sha256
- name: docker
image: plugins/docker
settings:
username:
from_secret: hub_user
password:
from_secret: hub_passwd
# auto_tag: true
mtu: 1000
# purge: true
repo: hub.kumoly.io/tools/configui
registry: hub.kumoly.io
trigger:
event: tag

17
Dockerfile Normal file
View File

@ -0,0 +1,17 @@
FROM golang:1.17.2-alpine3.14 as builder
RUN apk update && apk add --no-cache git tzdata
WORKDIR /src
COPY go.mod go.sum /src/
RUN go mod download
COPY . .
RUN VERSION=$(git describe --tags --abbrev=0) BUILD=$(git rev-parse --short HEAD) && \
GOOS=linux GOARCH=amd64 \
go build -ldflags "-X main.Version=${VERSION} -X main.Build=${BUILD} -w" \
-o /go/bin/breacher
FROM alpine:3.14
ENV PATH="/go/bin:${PATH}"
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /go/bin/breacher /go/bin/breacher
ENTRYPOINT ["/go/bin/breacher"]

View File

@ -1,12 +1,18 @@
package breacher
import (
"fmt"
"io"
"io/ioutil"
"log"
"net"
"os"
"strconv"
"strings"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
)
var (
@ -14,14 +20,19 @@ var (
sKey string
)
func init() {
sshCmd.Flags().StringVarP(&sPasswd, "password", "p", "", "login using password")
sshCmd.Flags().StringVarP(&sKey, "keyfile", "i", "", "login keyfile (path/to/file)")
}
var sshCmd = &cobra.Command{
Use: "tunnel [from address] [to address] [user@host:port]",
Short: "ssh tunneling to access remote services",
Long: `ssh tunneling to access remote services
ex.
breacher forward :8080 kumoly.io:5080
breacher forward :8080 :8000
breacher forward --udp :8080 192.168.51.211:53
breacher tunnel :8080 host:80 user@example.com -p paswd
breacher tunnel :8080 :80 user@example.com -i ~/.ssh/id_rsa
breacher tunnel :8080 kumoly.io:443 user@example.com
`,
Args: cobra.ExactArgs(3),
Run: func(cmd *cobra.Command, args []string) {
@ -42,12 +53,11 @@ breacher forward --udp :8080 192.168.51.211:53
log.Fatalln(err)
}
if localHost == "" {
localHost = "localhost"
localHost = "0.0.0.0"
}
if remoteHost == "" {
remoteHost = "localhost"
}
st := NewSSHTunnel(localHost, localPort, remoteHost, remotePort)
split := strings.Split(args[2], "@")
if len(split) != 2 {
log.Fatalln("ssh host name not valid")
@ -73,22 +83,179 @@ breacher forward --udp :8080 192.168.51.211:53
}
}
}
st.server.Host = sshHost
st.SetPort(sshPort)
st.SetUser(usr)
st.SetPassword("ubuntu")
st.SetDebug(true)
st.SetConnState(func(tun *SSHTun, state ConnState) {
switch state {
case StateStarting:
log.Printf("STATE is Starting")
case StateStarted:
log.Printf("STATE is Started")
case StateStopped:
log.Printf("STATE is Stopped")
var auth ssh.AuthMethod
if sPasswd != "" {
auth = ssh.Password(sPasswd)
} else if sKey != "" {
auth = PrivateKeyFile(sKey)
} else {
auth = SSHAgent()
}
})
st := NewSSHTunnel(
&Endpoint{localHost, localPort, ""},
&Endpoint{remoteHost, remotePort, ""},
&Endpoint{sshHost, sshPort, usr},
auth,
)
log.Fatalln(st.Start())
},
}
type Endpoint struct {
Host string
Port int
User string
}
func (endpoint *Endpoint) String() string {
return fmt.Sprintf("%s:%d", endpoint.Host, endpoint.Port)
}
type SSHTunnel struct {
Local *Endpoint
Server *Endpoint
Remote *Endpoint
Config *ssh.ClientConfig
Conns []net.Conn
SvrConns []*ssh.Client
isOpen bool
close chan interface{}
}
func newConnectionWaiter(listener net.Listener, c chan net.Conn) {
conn, err := listener.Accept()
if err != nil {
fmt.Println(err)
return
}
c <- conn
}
func (tunnel *SSHTunnel) Start() error {
listener, err := net.Listen("tcp", tunnel.Local.String())
if err != nil {
return err
}
tunnel.isOpen = true
tunnel.Local.Port = listener.Addr().(*net.TCPAddr).Port
for {
if !tunnel.isOpen {
break
}
c := make(chan net.Conn)
go newConnectionWaiter(listener, c)
log.Println("listening for new connections...")
select {
case <-tunnel.close:
log.Println("close signal received, closing...")
tunnel.isOpen = false
case conn := <-c:
tunnel.Conns = append(tunnel.Conns, conn)
log.Println("accepted connection")
go tunnel.forward(conn)
}
}
var total int
total = len(tunnel.Conns)
for i, conn := range tunnel.Conns {
log.Printf("closing the netConn (%d of %d)\n", i+1, total)
err := conn.Close()
if err != nil {
log.Println(err.Error())
}
}
total = len(tunnel.SvrConns)
for i, conn := range tunnel.SvrConns {
log.Printf("closing the serverConn (%d of %d)\n", i+1, total)
err := conn.Close()
if err != nil {
log.Println(err.Error())
}
}
err = listener.Close()
if err != nil {
return err
}
log.Println("tunnel closed")
return nil
}
func (tunnel *SSHTunnel) forward(localConn net.Conn) {
serverConn, err := ssh.Dial("tcp", tunnel.Server.String(), tunnel.Config)
if err != nil {
log.Printf("server dial error: %s\n", err)
return
}
log.Printf("connected to %s (1 of 2)\n", tunnel.Server.String())
tunnel.SvrConns = append(tunnel.SvrConns, serverConn)
remoteConn, err := serverConn.Dial("tcp", tunnel.Remote.String())
if err != nil {
log.Printf("remote dial error: %s\n", err)
return
}
tunnel.Conns = append(tunnel.Conns, remoteConn)
log.Printf("connected to %s (2 of 2)\n", tunnel.Remote.String())
copyConn := func(writer, reader net.Conn) {
_, err := io.Copy(writer, reader)
if err != nil {
log.Printf("io.Copy error: %s\n", err)
}
}
go copyConn(localConn, remoteConn)
go copyConn(remoteConn, localConn)
}
func (tunnel *SSHTunnel) Close() {
tunnel.close <- struct{}{}
}
// NewSSHTunnel creates a new single-use tunnel. Supplying "0" for localport will use a random port.
func NewSSHTunnel(from, to, server *Endpoint, auth ssh.AuthMethod) *SSHTunnel {
if server.Port == 0 {
server.Port = 22
}
sshTunnel := &SSHTunnel{
Config: &ssh.ClientConfig{
User: server.User,
Auth: []ssh.AuthMethod{auth},
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
// Always accept key.
return nil
},
},
Local: from,
Server: server,
Remote: to,
close: make(chan interface{}),
}
return sshTunnel
}
func PrivateKeyFile(file string) ssh.AuthMethod {
buffer, err := ioutil.ReadFile(file)
if err != nil {
return nil
}
key, err := ssh.ParsePrivateKey(buffer)
if err != nil {
return nil
}
return ssh.PublicKeys(key)
}
func SSHAgent() ssh.AuthMethod {
if sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")); err == nil {
return ssh.PublicKeysCallback(agent.NewClient(sshAgent).Signers)
}
return nil
}

View File

@ -1,499 +0,0 @@
package breacher
import (
"context"
"fmt"
"io"
"log"
"net"
"os"
"sync"
"time"
"io/ioutil"
"os/user"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
)
type Endpoint struct {
Host string
Port int
UnixSocket string
}
func (e *Endpoint) connectionString() string {
if e.UnixSocket != "" {
return e.UnixSocket
}
return fmt.Sprintf("%s:%d", e.Host, e.Port)
}
func (e *Endpoint) connectionType() string {
if e.UnixSocket != "" {
return "unix"
}
return "tcp"
}
// AuthType is the type of authentication to use for SSH.
type AuthType int
const (
// AuthTypeKeyFile uses the keys from a SSH key file read from the system.
AuthTypeKeyFile AuthType = iota
// AuthTypeEncryptedKeyFile uses the keys from an encrypted SSH key file read from the system.
AuthTypeEncryptedKeyFile
// AuthTypeKeyReader uses the keys from a SSH key reader.
AuthTypeKeyReader
// AuthTypeEncryptedKeyReader uses the keys from an encrypted SSH key reader.
AuthTypeEncryptedKeyReader
// AuthTypePassword uses a password directly.
AuthTypePassword
// AuthTypeSSHAgent will use registered users in the ssh-agent.
AuthTypeSSHAgent
// AuthTypeAuto tries to get the authentication method automatically. See SSHTun.Start for details on
// this.
AuthTypeAuto
)
// SSHTun represents a SSH tunnel
type SSHTun struct {
*sync.Mutex
ctx context.Context
cancel context.CancelFunc
errCh chan error
user string
authType AuthType
authKeyFile string
authKeyReader io.Reader
authPassword string
server Endpoint
local Endpoint
remote Endpoint
started bool
timeout time.Duration
debug bool
connState func(*SSHTun, ConnState)
}
// ConnState represents the state of the SSH tunnel. It's returned to an optional function provided to SetConnState.
type ConnState int
const (
// StateStopped represents a stopped tunnel. A call to Start will make the state to transition to StateStarting.
StateStopped ConnState = iota
// StateStarting represents a tunnel initializing and preparing to listen for connections.
// A successful initialization will make the state to transition to StateStarted, otherwise it will transition to StateStopped.
StateStarting
// StateStarted represents a tunnel ready to accept connections.
// A call to stop or an error will make the state to transition to StateStopped.
StateStarted
)
// New creates a new SSH tunnel to the specified server redirecting a port on local localhost to a port on remote localhost.
// By default the SSH connection is made to port 22 as root and using automatic detection of the authentication
// method (see Start for details on this).
// Calling SetPassword will change the authentication to password based.
// Calling SetKeyFile will change the authentication to keyfile based with an optional key file.
// The SSH user and port can be changed with SetUser and SetPort.
// The local and remote hosts can be changed to something different than localhost with SetLocalHost and SetRemoteHost.
// The states of the tunnel can be received throgh a callback function with SetConnState.
func NewSSHTunnel(localHost string, localPort int, remoteHost string, remotePort int) *SSHTun {
return &SSHTun{
Mutex: &sync.Mutex{},
server: Endpoint{
Host: "",
Port: 22,
},
user: "root",
authType: AuthTypeAuto,
authKeyFile: "",
authPassword: "",
local: Endpoint{
Host: localHost,
Port: localPort,
},
remote: Endpoint{
Host: remoteHost,
Port: remotePort,
},
started: false,
timeout: time.Second * 15,
debug: false,
}
}
func NewUnix(localUnixSocket string, server string, remoteUnixSocket string) *SSHTun {
return &SSHTun{
Mutex: &sync.Mutex{},
server: Endpoint{
Host: server,
Port: 22,
},
user: "root",
authType: AuthTypeAuto,
authKeyFile: "",
authPassword: "",
local: Endpoint{
UnixSocket: localUnixSocket,
},
remote: Endpoint{
UnixSocket: remoteUnixSocket,
},
started: false,
timeout: time.Second * 15,
debug: false,
}
}
// SetPort changes the port where the SSH connection will be made.
func (tun *SSHTun) SetPort(port int) {
tun.server.Port = port
}
// SetUser changes the user used to make the SSH connection.
func (tun *SSHTun) SetUser(user string) {
tun.user = user
}
// SetKeyFile changes the authentication to key-based and uses the specified file.
// Leaving it empty defaults to the default linux private key location ($HOME/.ssh/id_rsa).
func (tun *SSHTun) SetKeyFile(file string) {
tun.authType = AuthTypeKeyFile
tun.authKeyFile = file
}
// SetEncryptedKeyFile changes the authentication to encrypted key-based and uses the specified file and password.
// Leaving it empty defaults to the default linux private key location ($HOME/.ssh/id_rsa).
func (tun *SSHTun) SetEncryptedKeyFile(file string, password string) {
tun.authType = AuthTypeEncryptedKeyFile
tun.authKeyFile = file
tun.authPassword = password
}
// SetKeyReader changes the authentication to key-based and uses the specified reader.
// Leaving it empty defaults to the default linux private key location ($HOME/.ssh/id_rsa).
func (tun *SSHTun) SetKeyReader(reader io.Reader) {
tun.authType = AuthTypeKeyReader
tun.authKeyReader = reader
}
// SetEncryptedKeyReader changes the authentication to encrypted key-based and uses the specified reader and password.
// Leaving it empty defaults to the default linux private key location ($HOME/.ssh/id_rsa).
func (tun *SSHTun) SetEncryptedKeyReader(reader io.Reader, password string) {
tun.authType = AuthTypeEncryptedKeyReader
tun.authKeyReader = reader
tun.authPassword = password
}
// SetSSHAgent changes the authentication to ssh-agent.
func (tun *SSHTun) SetSSHAgent() {
tun.authType = AuthTypeSSHAgent
}
// SetPassword changes the authentication to password-based and uses the specified password.
func (tun *SSHTun) SetPassword(password string) {
tun.authType = AuthTypePassword
tun.authPassword = password
}
// SetLocalHost sets the local host to redirect (defaults to localhost)
func (tun *SSHTun) SetLocalHost(host string) {
tun.local.Host = host
}
// SetRemoteHost sets the remote host to redirect (defaults to localhost)
func (tun *SSHTun) SetRemoteHost(host string) {
tun.remote.Host = host
}
// SetTimeout sets the connection timeouts (defaults to 15 seconds).
func (tun *SSHTun) SetTimeout(timeout time.Duration) {
tun.timeout = timeout
}
// SetDebug enables or disables log messages (disabled by default).
func (tun *SSHTun) SetDebug(debug bool) {
tun.debug = debug
}
// SetConnState specifies an optional callback function that is called when a SSH tunnel changes state.
// See the ConnState type and associated constants for details.
func (tun *SSHTun) SetConnState(connStateFun func(*SSHTun, ConnState)) {
tun.connState = connStateFun
}
// Start starts the SSH tunnel. After this call, all Set* methods will have no effect until Close is called.
// Note on SSH authentication: in case the tunnel's authType is set to AuthTypeAuto the following will happen:
// The default key file will be used, if that doesn't succeed it will try to use the SSH agent.
// If that fails the whole authentication fails.
// That means if you want to use password or encrypted key file authentication, you have to specify that explicitly.
func (tun *SSHTun) Start() error {
tun.Lock()
if tun.connState != nil {
tun.connState(tun, StateStarting)
}
// SSH config
config, err := tun.initSSHConfig()
if err != nil {
return tun.errNotStarted(err)
}
local := tun.local.connectionString()
// Local listener
localList, err := net.Listen(tun.local.connectionType(), local)
if err != nil {
return tun.errNotStarted(fmt.Errorf("local listen on %s failed: %s", local, err.Error()))
}
// Context and error channel
tun.ctx, tun.cancel = context.WithCancel(context.Background())
tun.errCh = make(chan error)
// Accept connections
go func() {
for {
localConn, err := localList.Accept()
if err != nil {
tun.errStarted(fmt.Errorf("local accept on %s failed: %s", local, err.Error()))
break
}
if tun.debug {
log.Printf("Accepted connection from %s", localConn.RemoteAddr().String())
}
// Launch the forward
go tun.forward(localConn, config)
}
}()
// Wait until someone cancels the context and stop accepting connections
go func() {
<-tun.ctx.Done()
localList.Close()
}()
// Now others can call Stop or fail
if tun.debug {
log.Printf("Listening on %s", local)
}
tun.started = true
if tun.connState != nil {
tun.connState(tun, StateStarted)
}
tun.Unlock()
// Wait to exit
errFromCh := <-tun.errCh
return errFromCh
}
func (tun *SSHTun) errNotStarted(err error) error {
tun.started = false
if tun.connState != nil {
tun.connState(tun, StateStopped)
}
tun.Unlock()
return err
}
func (tun *SSHTun) errStarted(err error) {
tun.Lock()
if tun.started {
tun.cancel()
if tun.connState != nil {
tun.connState(tun, StateStopped)
}
tun.started = false
tun.errCh <- err
}
tun.Unlock()
}
func (tun *SSHTun) initSSHConfig() (*ssh.ClientConfig, error) {
config := &ssh.ClientConfig{
User: tun.user,
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
Timeout: tun.timeout,
}
authMethod, err := tun.getSSHAuthMethod()
if err != nil {
return nil, err
}
config.Auth = []ssh.AuthMethod{authMethod}
return config, nil
}
func (tun *SSHTun) getSSHAuthMethod() (ssh.AuthMethod, error) {
switch tun.authType {
case AuthTypeKeyFile:
return tun.getSSHAuthMethodForKeyFile(false)
case AuthTypeEncryptedKeyFile:
return tun.getSSHAuthMethodForKeyFile(true)
case AuthTypeKeyReader:
return tun.getSSHAuthMethodForKeyReader(false)
case AuthTypeEncryptedKeyReader:
return tun.getSSHAuthMethodForKeyReader(true)
case AuthTypePassword:
return ssh.Password(tun.authPassword), nil
case AuthTypeSSHAgent:
return tun.getSSHAuthMethodForSSHAgent()
case AuthTypeAuto:
method, err := tun.getSSHAuthMethodForKeyFile(false)
if err != nil {
return tun.getSSHAuthMethodForSSHAgent()
}
return method, nil
default:
return nil, fmt.Errorf("unknown auth type: %d", tun.authType)
}
}
func (tun *SSHTun) getSSHAuthMethodForKeyFile(encrypted bool) (ssh.AuthMethod, error) {
if tun.authKeyFile == "" {
usr, _ := user.Current()
if usr != nil {
tun.authKeyFile = usr.HomeDir + "/.ssh/id_rsa"
} else {
tun.authKeyFile = "/root/.ssh/id_rsa"
}
}
buf, err := ioutil.ReadFile(tun.authKeyFile)
if err != nil {
return nil, fmt.Errorf("error reading SSH key file %s: %s", tun.authKeyFile, err.Error())
}
key, err := tun.parsePrivateKey(buf, encrypted)
if err != nil {
return nil, fmt.Errorf("error reading SSH key file %s: %s", tun.authKeyFile, err.Error())
}
return key, nil
}
func (tun *SSHTun) getSSHAuthMethodForKeyReader(encrypted bool) (ssh.AuthMethod, error) {
buf, err := ioutil.ReadAll(tun.authKeyReader)
if err != nil {
return nil, fmt.Errorf("error reading from SSH key reader: %s", err.Error())
}
key, err := tun.parsePrivateKey(buf, encrypted)
if err != nil {
return nil, fmt.Errorf("error reading from SSH key reader: %s", err.Error())
}
return key, nil
}
func (tun *SSHTun) parsePrivateKey(buf []byte, encrypted bool) (ssh.AuthMethod, error) {
var key ssh.Signer
var err error
if encrypted {
key, err = ssh.ParsePrivateKeyWithPassphrase(buf, []byte(tun.authPassword))
if err != nil {
return nil, fmt.Errorf("error parsing encrypted key: %s", err.Error())
}
} else {
key, err = ssh.ParsePrivateKey(buf)
if err != nil {
return nil, fmt.Errorf("error parsing key: %s", err.Error())
}
}
return ssh.PublicKeys(key), nil
}
func (tun *SSHTun) getSSHAuthMethodForSSHAgent() (ssh.AuthMethod, error) {
conn, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK"))
if err != nil {
return nil, fmt.Errorf("error opening unix socket: %s", err)
}
agentClient := agent.NewClient(conn)
signers, err := agentClient.Signers()
if err != nil {
return nil, fmt.Errorf("error getting ssh-agent signers: %s", err)
}
if len(signers) == 0 {
return nil, fmt.Errorf("no signers from ssh-agent. Use 'ssh-add' to add keys to agent")
}
return ssh.PublicKeys(signers...), nil
}
func (tun *SSHTun) forward(localConn net.Conn, config *ssh.ClientConfig) {
defer localConn.Close()
local := tun.local.connectionString()
server := tun.server.connectionString()
remote := tun.remote.connectionString()
sshConn, err := ssh.Dial(tun.server.connectionType(), server, config)
if err != nil {
tun.errStarted(fmt.Errorf("SSH connection to %s failed: %s", server, err.Error()))
return
}
defer sshConn.Close()
if tun.debug {
log.Printf("SSH connection to %s done", server)
}
remoteConn, err := sshConn.Dial(tun.remote.connectionType(), remote)
if err != nil {
if tun.debug {
log.Printf("Remote dial to %s failed: %s", remote, err.Error())
}
return
}
defer remoteConn.Close()
if tun.debug {
log.Printf("Remote connection to %s done", remote)
}
connStr := fmt.Sprintf("%s -(tcp)> %s -(ssh)> %s -(tcp)> %s", localConn.RemoteAddr().String(), local, server, remote)
if tun.debug {
log.Printf("SSH tunnel OPEN: %s", connStr)
}
myCtx, myCancel := context.WithCancel(tun.ctx)
go func() {
_, err = io.Copy(remoteConn, localConn)
if err != nil {
//log.Printf("Error on io.Copy remote->local on connection %s: %s", connStr, err.Error())
myCancel()
return
}
}()
go func() {
_, err = io.Copy(localConn, remoteConn)
if err != nil {
//log.Printf("Error on io.Copy local->remote on connection %s: %s", connStr, err.Error())
myCancel()
return
}
}()
<-myCtx.Done()
myCancel()
if tun.debug {
log.Printf("SSH tunnel CLOSE: %s", connStr)
}
}
// Stop closes the SSH tunnel and its connections.
// After this call all Set* methods will have effect and Start can be called again.
func (tun *SSHTun) Stop() {
tun.errStarted(nil)
}

34
build.sh Normal file
View File

@ -0,0 +1,34 @@
VERSION=$(git describe --tags --abbrev=0)
if [ $? -ne 0 ]; then VERSION=$DRONE_TAG; fi
BUILD=$(git rev-parse --short HEAD)
if [ $? -ne 0 ]; then BUILD=${DRONE_COMMIT:0:7}; fi
PROJ=breacher
HUB=hub.kumoly.io
HUB_PROJECT=tools
DIST=dist
LDFLAGS="-ldflags \"-X main.Version=${VERSION} -X main.Build=${BUILD} -w\""
FAILURES=""
PLATFORMS="darwin/amd64 darwin/arm64"
PLATFORMS="$PLATFORMS windows/amd64"
PLATFORMS="$PLATFORMS linux/amd64"
for PLATFORM in $PLATFORMS; do
GOOS=${PLATFORM%/*}
GOARCH=${PLATFORM#*/}
BIN_FILENAME="${PROJ}"
if [[ "${GOOS}" == "windows" ]]; then BIN_FILENAME="${BIN_FILENAME}.exe"; fi
CMD="GOOS=${GOOS} GOARCH=${GOARCH} go build ${LDFLAGS} -o ${DIST}/${BIN_FILENAME} $@"
echo "${CMD}"
eval $CMD || FAILURES="${FAILURES} ${PLATFORM}"
sh -c "cd ${DIST} && tar -czf ${PROJ}-${VERSION}-${GOOS}-${GOARCH}.tar.gz ${BIN_FILENAME} && rm ${BIN_FILENAME}"
done
if [[ "${FAILURES}" != "" ]]; then
echo ""
echo "${SCRIPT_NAME} failed on: ${FAILURES}"
exit 1
fi