docs: update docs and other fixes
This commit is contained in:
parent
5d111f0885
commit
09ab41650d
|
@ -1 +0,0 @@
|
||||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCg0nrgmQO7Cxt34uuI9pqZq72nqY/CmcTf/ZAtyZydk3kS9JZBckk0iUcT+zWWDcImVw9GWrkAu58mzAMU97LrrVg7Ap+pAZl99UNOUNkC8HeJTN559l/eCZ6gohlAlaKV3qSWia7+aIwRabhjTPAd6oOuaQjifd+/uvfQUTHIIjzsM6g3fQz70IyilH7Iid0OXytNWLh0emRjE/qcJA0R27cGQXxiQEf8Pz6Tl1YFbp3qd7pk0scl8kobdKmNv6LSPUUNuSeJC3AMbrmiHp9XnVTUBh0PB8Yn6kqA0OCtNZ/29RI4boIyDI4Y+wJxZ65E45doEExRPyxMzeoEnaAacUieu97rzGRj0WvfBIKs8kGCPJ8JA9ZvFcWkQbZK2kIQ/AWk8utmpKewlUzdB3VBM2d5nrsr+zo7U92rD7GUy0SEpV7sNy9pmob8M9WB8Qnjtl7+VmC1w5yNnJCfrzXVo+k7RsL71m/EbbJZj35hd6ExhPLgSL+CFRl4sj6TqJrdFYH0LbhQqvTydc8R8N9MhaMxVdEkQ08M3KcjHgFh2jEDJGbd4cChcPt+jWcWKw34Ycf8R7WKPvB1v9FVYOxesBMCaElWLMdrQlVf118WjJ+A25ynCwCpzYOFfsRCb0JbH1e2ANOXV2HdXvTBQxtT6r1MUFVKWJPrfgWlAdA3mQ== jonathan.lundy@ip-10-240-248-37.ec2.internal
|
|
30
README.md
30
README.md
|
@ -1,6 +1,6 @@
|
||||||
# sshfwd
|
# sshfwd
|
||||||
|
|
||||||
This is a reverse forward service that uses SSH as the transport. It works similar to ngrok or localtunnel.me.
|
This is a reverse proxy service that uses SSH as the transport. It works similar to ngrok or localtunnel.me.
|
||||||
|
|
||||||
You run the service on a internet addressible host and ssh to it. Using ssh remote forwards (ie. ssh -R) the port on the remote host will be forwared to
|
You run the service on a internet addressible host and ssh to it. Using ssh remote forwards (ie. ssh -R) the port on the remote host will be forwared to
|
||||||
the configured port on your local machine.
|
the configured port on your local machine.
|
||||||
|
@ -9,21 +9,31 @@ on Remote host:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ make genkeys # generate the services host keys.
|
$ make genkeys # generate the services host keys.
|
||||||
$ SSH_HOSTKEYS=hostkeys SSH_LISTEN=:2222 sshfwd # run service on port 2222
|
$ SSH_HOSTKEYS=hostkeys SSH_LISTEN=:2222 SSH_DOMAIN=example.com sshfwd # run service on port 2222
|
||||||
```
|
```
|
||||||
|
|
||||||
on your local machine:
|
For best results place this behind a TLS termination that has a wildcard certificate and CNAME for `*.yourdomain.com`
|
||||||
|
|
||||||
|
|
||||||
|
on your local machine have a ssh private and public key available:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ ssh -T remote.example.com -p 2222 -R 0.0.0.0:1234:localhost:3000
|
$ export LOCAL_PORT=3000; export PRIV_KEY=~/.ssh/id_ed25519; sh -c "$(shell http --form POST example.com:2222 pub=@$(PRIV_KEY).pub)"
|
||||||
```
|
```
|
||||||
|
|
||||||
now if you access `remote.example.com:1234` it will be the same as accessing `localhost:3000`
|
This will setup a reverse proxy on the example host that you can then use to access the local port. It will print a name unique to your ssh key.
|
||||||
|
|
||||||
# Pubkeys
|
|
||||||
|
|
||||||
if the env variable `SSH_AUTHKEYS` is set it will require that the client authenticates with one of the keys in the `SSH_AUTHKEYS` directory.
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ SSH_LISTEN=:2222 SSH_HOSTKEYS=hostkeys SSH_AUTHKEYS=authkeys sshfwd
|
$ http GET romeo-nine-lake.example.com:2222
|
||||||
|
```
|
||||||
|
|
||||||
|
All accesses to the proxy will have the HTTP request printed out to the ssh connection.
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /connect HTTP/1.1
|
||||||
|
Host: romeo-nine-lake.example.com
|
||||||
|
Accept: */*
|
||||||
|
User-Agent: curl/7.64.1
|
||||||
|
X-Forwarded-Host: romeo-nine-lake.example.com
|
||||||
|
X-Origin-Host: [::1]:7000
|
||||||
```
|
```
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKk9O/oAyP6OZetRFQUjjJeeQAZLqmlPCUwNvJzmAjwR jonlundy@MB-C02F1Q5XMD6M
|
|
|
@ -1 +0,0 @@
|
||||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIUSB+REbjTVPRKuHHCRaMJJlMIw6Ofp0oZvy7wDSZR4 jonathan.lundy@US17020047.local
|
|
116
main.go
116
main.go
|
@ -25,11 +25,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
domainName = "prox.int"
|
domainName = "prox.int"
|
||||||
domainSuffix = ".prox.int"
|
portRange = "7000-7999"
|
||||||
portStart uint32 = 7000
|
bindHost = "[::1]"
|
||||||
portEnd uint32 = 7999
|
|
||||||
bindHost = "[::1]"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var filterName = regexp.MustCompile("[^a-z0-9-]+")
|
var filterName = regexp.MustCompile("[^a-z0-9-]+")
|
||||||
|
@ -53,21 +51,19 @@ func run(ctx context.Context) {
|
||||||
ssh.NoPty(),
|
ssh.NoPty(),
|
||||||
)
|
)
|
||||||
|
|
||||||
files, err := ioutil.ReadDir(envMust("SSH_HOSTKEYS"))
|
hostKeys := envMust("SSH_HOSTKEYS")
|
||||||
|
files, err := ioutil.ReadDir(hostKeys)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
for _, f := range files {
|
||||||
|
opts = append(opts, ssh.HostKeyFile(filepath.Join(hostKeys, f.Name())))
|
||||||
|
}
|
||||||
|
|
||||||
srv := &server{
|
srv := &server{
|
||||||
bindHost: envDefault("SSH_HOST", bindHost),
|
bindHost: envDefault("SSH_HOST", bindHost),
|
||||||
portStart: portStart,
|
|
||||||
portEnd: portEnd,
|
|
||||||
domainName: envDefault("SSH_DOMAIN", domainName),
|
domainName: envDefault("SSH_DOMAIN", domainName),
|
||||||
domainSuffix: envDefault("SSH_DOMAIN_SUFFIX", domainSuffix),
|
domainSuffix: envDefault("SSH_DOMAIN_SUFFIX", "."+domainName),
|
||||||
}
|
|
||||||
|
|
||||||
for _, f := range files {
|
|
||||||
opts = append(opts, ssh.HostKeyFile(filepath.Join(envMust("SSH_HOSTKEYS"), f.Name())))
|
|
||||||
}
|
}
|
||||||
opts = append(opts, srv.optAuthUser()...)
|
opts = append(opts, srv.optAuthUser()...)
|
||||||
|
|
||||||
|
@ -81,7 +77,31 @@ func run(ctx context.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Fatal(mux.Serve(ctx))
|
if r := envDefault("SSH_PORTRANGE", portRange); r != "" {
|
||||||
|
sp := strings.SplitN(r, "-", 2)
|
||||||
|
if len(sp) == 1 {
|
||||||
|
log.Fatal("SSH_PORTRANGE should have start and end like 7000-7999")
|
||||||
|
}
|
||||||
|
|
||||||
|
var p uint64
|
||||||
|
if p, err = strconv.ParseUint(sp[0], 10, 32); err != nil {
|
||||||
|
log.Fatal("SSH_PORTRANGE start port invalid:", sp[0])
|
||||||
|
}
|
||||||
|
srv.portStart = uint32(p)
|
||||||
|
|
||||||
|
if p, err = strconv.ParseUint(sp[1], 10, 32); err != nil {
|
||||||
|
log.Fatal("SSH_PORTRANGE end port invalid:", sp[1])
|
||||||
|
}
|
||||||
|
srv.portEnd = uint32(p)
|
||||||
|
|
||||||
|
if srv.portStart > srv.portEnd {
|
||||||
|
log.Fatalf("SSH_PORTRANGE is reversed %d > %d", srv.portStart, srv.portEnd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = mux.Serve(ctx); err != nil {
|
||||||
|
log.Fatal()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func envMust(s string) string {
|
func envMust(s string) string {
|
||||||
|
@ -95,7 +115,7 @@ func envMust(s string) string {
|
||||||
func envDefault(s, d string) string {
|
func envDefault(s, d string) string {
|
||||||
v := os.Getenv(s)
|
v := os.Getenv(s)
|
||||||
if v == "" {
|
if v == "" {
|
||||||
return d
|
v = d
|
||||||
}
|
}
|
||||||
log.Println("env", s, "==", v)
|
log.Println("env", s, "==", v)
|
||||||
return v
|
return v
|
||||||
|
@ -110,7 +130,6 @@ func (srv *server) newSession(ctx context.Context) func(ssh.Session) {
|
||||||
if u, ok := srv.GetUserByName(s.User()); ok {
|
if u, ok := srv.GetUserByName(s.User()); ok {
|
||||||
host := fmt.Sprintf("%v:%v", u.bindHost, u.bindPort)
|
host := fmt.Sprintf("%v:%v", u.bindHost, u.bindPort)
|
||||||
director := func(req *http.Request) {
|
director := func(req *http.Request) {
|
||||||
req = req.WithContext(s.Context())
|
|
||||||
if h := req.Header.Get("X-Forwarded-Host"); h == "" {
|
if h := req.Header.Get("X-Forwarded-Host"); h == "" {
|
||||||
req.Header.Set("X-Forwarded-Host", req.Host)
|
req.Header.Set("X-Forwarded-Host", req.Host)
|
||||||
}
|
}
|
||||||
|
@ -120,12 +139,12 @@ func (srv *server) newSession(ctx context.Context) func(ssh.Session) {
|
||||||
|
|
||||||
requestDump, err := httputil.DumpRequest(req, req.Method == http.MethodPost || req.Method == http.MethodPut)
|
requestDump, err := httputil.DumpRequest(req, req.Method == http.MethodPost || req.Method == http.MethodPut)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
fmt.Fprintln(s, string(requestDump))
|
fmt.Fprintln(s, string(requestDump))
|
||||||
}
|
}
|
||||||
u.proxy = &httputil.ReverseProxy{Director: director}
|
u.proxy = &httputil.ReverseProxy{Director: director}
|
||||||
fmt.Fprintf(s, "Created HTTP listener at: %v%v\n", u.name, srv.domainSuffix)
|
fmt.Fprintf(s, "Created HTTP listener at: %v%v\n\n", u.name, srv.domainSuffix)
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
|
@ -136,6 +155,8 @@ func (srv *server) newSession(ctx context.Context) func(ssh.Session) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if u, ok := srv.GetUserByName(s.User()); ok {
|
if u, ok := srv.GetUserByName(s.User()); ok {
|
||||||
|
u.ctx = nil
|
||||||
|
u.proxy = nil
|
||||||
srv.ports.Delete(u.bindPort)
|
srv.ports.Delete(u.bindPort)
|
||||||
}
|
}
|
||||||
if _, err := fmt.Fprintf(s, "Goodbye! %s\n", s.User()); err != nil {
|
if _, err := fmt.Fprintf(s, "Goodbye! %s\n", s.User()); err != nil {
|
||||||
|
@ -158,18 +179,43 @@ type server struct {
|
||||||
users sync.Map
|
users sync.Map
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *server) String() string {
|
||||||
|
var b strings.Builder
|
||||||
|
fmt.Fprintln(&b, "Server: ", s.domainName)
|
||||||
|
fmt.Fprintln(&b, " Port: ", s.listenPort)
|
||||||
|
fmt.Fprintln(&b, " Suffix: ", s.domainSuffix)
|
||||||
|
fmt.Fprintln(&b, " BindHost: ", s.bindHost)
|
||||||
|
fmt.Fprintf(&b, " PortRange: %d-%d\n", s.portStart, s.portEnd)
|
||||||
|
fmt.Fprintln(&b, " NextPort: ", s.portNext)
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
type user struct {
|
type user struct {
|
||||||
name string
|
name string
|
||||||
pubkey ssh.PublicKey
|
pubkey ssh.PublicKey
|
||||||
bindHost string
|
bindHost string
|
||||||
bindPort uint32
|
bindPort uint32
|
||||||
ctx ssh.Context
|
ctx ssh.Context
|
||||||
proxy http.Handler
|
proxy http.Handler
|
||||||
|
lastLogin time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *user) String() string {
|
||||||
|
var b strings.Builder
|
||||||
|
fmt.Fprintln(&b, "User: ", u.name)
|
||||||
|
fmt.Fprintf(&b, " Ptr: %p\n", u)
|
||||||
|
fmt.Fprintf(&b, " Pubkey: %x\n", u.pubkey)
|
||||||
|
fmt.Fprintln(&b, " Host: ", u.bindHost)
|
||||||
|
fmt.Fprintln(&b, " Port: ", u.bindPort)
|
||||||
|
fmt.Fprintf(&b, " Active: %t\n", u.ctx != nil)
|
||||||
|
fmt.Fprintln(&b, " LastLog:", u.lastLogin)
|
||||||
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *server) AddUser(pubkey ssh.PublicKey) *user {
|
func (srv *server) AddUser(pubkey ssh.PublicKey) *user {
|
||||||
u := &user{}
|
u := &user{}
|
||||||
|
|
||||||
|
u.lastLogin = time.Now()
|
||||||
u.name = fingerprintHuman(pubkey)
|
u.name = fingerprintHuman(pubkey)
|
||||||
u.name = strings.ToLower(u.name)
|
u.name = strings.ToLower(u.name)
|
||||||
u.name = filterName.ReplaceAllString(u.name, "")
|
u.name = filterName.ReplaceAllString(u.name, "")
|
||||||
|
@ -248,9 +294,13 @@ func (srv *server) optAuthUser() []ssh.Option {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ssh.KeysEqual(key, u.pubkey) {
|
if ssh.KeysEqual(key, u.pubkey) {
|
||||||
log.Println("User: ", ctx.User(), "Authorized:", u.bindHost, u.bindPort, ctx.ClientVersion(), ctx.SessionID(), ctx.LocalAddr(), ctx.RemoteAddr())
|
log.Println("User:", ctx.User(), "Authorized:", u.bindHost, u.bindPort, ctx.ClientVersion(), ctx.SessionID(), ctx.LocalAddr(), ctx.RemoteAddr())
|
||||||
u.ctx = ctx
|
u.ctx = ctx
|
||||||
srv.ports.Store(u.bindPort, u)
|
u.lastLogin = time.Now()
|
||||||
|
if _, loaded := srv.ports.LoadOrStore(u.bindPort, u); loaded {
|
||||||
|
log.Println("User:", ctx.User(), "already connected!")
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -306,7 +356,7 @@ func (srv *server) serveHTTP(ctx context.Context) func(net.Listener) error {
|
||||||
ReadTimeout: 2500 * time.Millisecond,
|
ReadTimeout: 2500 * time.Millisecond,
|
||||||
WriteTimeout: 5 * time.Second,
|
WriteTimeout: 5 * time.Second,
|
||||||
Handler: http.DefaultServeMux,
|
Handler: http.DefaultServeMux,
|
||||||
BaseContext: func(net.Listener) context.Context { return ctx },
|
BaseContext: func(net.Listener) context.Context { return ctx },
|
||||||
}
|
}
|
||||||
|
|
||||||
go func(ctx context.Context) {
|
go func(ctx context.Context) {
|
||||||
|
@ -338,16 +388,20 @@ func (srv *server) handleHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
u := srv.AddUser(pubkey)
|
u := srv.AddUser(pubkey)
|
||||||
rw.WriteHeader(201)
|
rw.WriteHeader(201)
|
||||||
fmt.Fprintf(rw, `ssh -T -p %v %v@%v -R "%v:%v:localhost:$LOCAL_PORT" -i $PRIV_KEY`, srv.listenPort, u.name, srv.domainName, u.bindHost, u.bindPort)
|
fmt.Fprintf(rw, `ssh -T -p %v %v@%v -R "%v:%v:localhost:$LOCAL_PORT" -i $PRIV_KEY`+"\n", srv.listenPort, u.name, srv.domainName, u.bindHost, u.bindPort)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintln(rw, "Hello!", r.Host, r.URL)
|
fmt.Fprintln(rw, "Hello!")
|
||||||
|
fmt.Fprintln(rw, srv)
|
||||||
|
fmt.Fprintln(rw, "Registered Users")
|
||||||
for _, u := range srv.ListUsers() {
|
for _, u := range srv.ListUsers() {
|
||||||
fmt.Fprintln(rw, "User:", u.name)
|
fmt.Fprintln(rw, u)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(rw, "Connected Users")
|
||||||
for _, u := range srv.ListConnectedUsers() {
|
for _, u := range srv.ListConnectedUsers() {
|
||||||
fmt.Fprintln(rw, "Conn:", u.name)
|
fmt.Fprintln(rw, u)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user