Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port pre-assignment #146

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,23 @@ sish@sish0:~/sish/pubkeys# curl https://github.com/antoniomika.keys > antoniomik
This will load my public keys from GitHub, place them in the directory that sish is watching,
and then load the pubkey. As soon as this command is run, I can SSH normally and it will authorize me.

### Port pre-assignment

If you use authentication keys, you can also pre-assign the port that a particular host can bind
with this option:

```--use-ports-from-keys```

It is slightly abusing the auth_keys specifications (sshd(8)) according to which you can specify
SSH options within the keys. Your line in pubkeys directory might look like this:

```permitlisten="12345" ssh-rsa THE_PUBKEY_IS_HERE comment@HOSTNAME```

sish will allow the host to use only this particular port for TCP forwarding.
This might be useful for managing multiple computers to which 3rd party might have access.
If you do this for every machine, none of them can block the pre-designated port dedicated for the other
even if somebody tried to mangle the settings.

## Custom domains

sish supports allowing users to bring custom domains to the service, but SSH key auth is required to be
Expand Down Expand Up @@ -297,6 +314,7 @@ Flags:
--debug Enable debugging information
-d, --domain string The root domain for HTTP(S) multiplexing that will be appended to subdomains (default "ssi.sh")
--force-requested-aliases Force the aliases used to be the one that is requested. Will fail the bind if it exists already
--use-ports-from-keys Ignore requested port and use the one specified as "permitlisten" option with the user's pubkey, if exists
--force-requested-ports Force the ports used to be the one that is requested. Will fail the bind if it exists already
--force-requested-subdomains Force the subdomains used to be the one that is requested. Will fail the bind if it exists already
--geodb Use a geodb to verify country IP address association for IP filtering
Expand Down
1 change: 1 addition & 0 deletions cmd/sish.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ func init() {
rootCmd.PersistentFlags().StringP("load-templates-directory", "", "templates/*", "The directory and glob parameter for templates that should be loaded")

rootCmd.PersistentFlags().BoolP("force-requested-ports", "", false, "Force the ports used to be the one that is requested. Will fail the bind if it exists already")
rootCmd.PersistentFlags().BoolP("use-ports-from-keys", "", false, "Ignore requested port and use the one specified as \"permitlisten\" option with the user's pubkey, if exists")
rootCmd.PersistentFlags().BoolP("force-requested-aliases", "", false, "Force the aliases used to be the one that is requested. Will fail the bind if it exists already")
rootCmd.PersistentFlags().BoolP("force-requested-subdomains", "", false, "Force the subdomains used to be the one that is requested. Will fail the bind if it exists already")
rootCmd.PersistentFlags().BoolP("bind-random-subdomains", "", true, "Force bound HTTP tunnels to use random subdomains instead of user provided ones")
Expand Down
1 change: 1 addition & 0 deletions sshmuxer/requests.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ func handleRemoteForward(newRequest *ssh.Request, sshConn *utils.SSHConnection,
if err != nil {
log.Println("Error replying to socket request:", err)
}

return
}

Expand Down
163 changes: 136 additions & 27 deletions utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
package utils

import (
"bytes"
"crypto/ed25519"
"crypto/rand"
"encoding/pem"
Expand Down Expand Up @@ -37,12 +36,18 @@ const (
sishDNSPrefix = "sish="
)

type SSHAuthKey struct {
santomet marked this conversation as resolved.
Show resolved Hide resolved
Key ssh.PublicKey
Options map[string][]string
Comment string
}

var (
// Filter is the IPFilter used to block connections.
Filter *ipfilter.IPFilter

// certHolder is a slice of publickeys for auth.
certHolder = make([]ssh.PublicKey, 0)
// certHolder is a map of SSHAuthKey objects, the key is string(Key.key.Marshal()).
certHolder = make(map[string]SSHAuthKey)

// holderLock is the mutex used to update the certHolder slice.
holderLock = sync.Mutex{}
Expand Down Expand Up @@ -127,7 +132,7 @@ func LoadProxyProtoConfig(l *proxyproto.Listener) {

// GetRandomPortInRange returns a random port in the provided range.
// The port range is a comma separated list of ranges or ports.
func GetRandomPortInRange(portRange string) uint32 {
func GetRandomPortInRange(portRange string, authPorts []uint32) uint32 {
var bindPort uint32

ranges := strings.Split(strings.TrimSpace(portRange), ",")
Expand Down Expand Up @@ -158,28 +163,55 @@ func GetRandomPortInRange(portRange string) uint32 {
}

mathrand.Seed(time.Now().UnixNano())
locHolder := mathrand.Intn(len(possible))

if len(possible[locHolder]) == 1 {
bindPort = uint32(possible[locHolder][0])
} else if len(possible[locHolder]) == 2 {
bindPort = uint32(mathrand.Intn(int(possible[locHolder][1]-possible[locHolder][0])) + int(possible[locHolder][0]))
}
if len(authPorts) > 0 {
bindPort = authPorts[mathrand.Intn(len(authPorts))]
antoniomika marked this conversation as resolved.
Show resolved Hide resolved
} else {
locHolder := mathrand.Intn(len(possible))

if len(possible[locHolder]) == 1 {
bindPort = uint32(possible[locHolder][0])
} else if len(possible[locHolder]) == 2 {
bindPort = uint32(mathrand.Intn(int(possible[locHolder][1]-possible[locHolder][0])) + int(possible[locHolder][0]))
}

}
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", bindPort))
if err != nil {
return GetRandomPortInRange(portRange)
if len(authPorts) == 1 {
return 0
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 will be a random port anyway. We want to still get a random port in a range (i.e. 1024-2048)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, my intention, in this case, is to block it. If all the ports in permitlisten are bind, you should not be able to create the tunnel

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bump

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to update the method to return an error then. I think returning 0 would still allow the tunnel to bind, wouldn't it?

}
if len(authPorts) > 1 {
for i, p := range authPorts {
if p == bindPort {
authPorts = append(authPorts[:i], authPorts[i+1:]...)
break
}
}
}
return GetRandomPortInRange(portRange, authPorts)
}

ln.Close()

return bindPort
}

//Check if port is in provided slice.
santomet marked this conversation as resolved.
Show resolved Hide resolved
func IsPortInSlice(p uint32, s []uint32) bool {

for _, a := range s {
if a == p {
return true
}
}
return false
}

// CheckPort verifies if a port exists within the port range.
// It will return 0 and an error if not (0 allows the kernel to select)
// the port.
func CheckPort(port uint32, portRanges string) (uint32, error) {
func CheckPort(port uint32, portRanges string, authPorts []uint32) (uint32, error) {
ranges := strings.Split(strings.TrimSpace(portRanges), ",")
checks := false
for _, r := range ranges {
Expand Down Expand Up @@ -213,6 +245,10 @@ func CheckPort(port uint32, portRanges string) (uint32, error) {
}
}

if len(authPorts) > 0 && !IsPortInSlice(port, authPorts) {
checks = false
}

if checks {
return port, nil
}
Expand Down Expand Up @@ -259,10 +295,42 @@ func WatchCerts() {
}
}

// parseSSHOptions parses options from ssh.ParseAuthorizedKey format to our map format for SSHAuthKey.
func parseSSHOptions(options []string) map[string][]string {
ret := make(map[string][]string)
for _, o := range options {
values := make([]string, 0)
optionSplit := strings.Split(o, "=")

if len(optionSplit) == 1 {
antoniomika marked this conversation as resolved.
Show resolved Hide resolved
ret[optionSplit[0]] = values
}
if len(optionSplit) == 2 {
v, ok := ret[optionSplit[0]]
if ok {
values = v
}

optionVal := optionSplit[1]
antoniomika marked this conversation as resolved.
Show resolved Hide resolved
if len(optionVal) > 0 && optionVal[0] == '"' {
optionVal = optionVal[1:]
}
if len(optionVal) > 0 && optionVal[len(optionVal)-1] == '"' {
optionVal = optionVal[:len(optionVal)-1]
}

values = append(values, optionVal)
ret[optionSplit[0]] = values
}

}
return ret
}

// loadCerts loads public keys from the keys directory into a slice that is used
// authenticating a user.
func loadCerts() {
tmpCertHolder := make([]ssh.PublicKey, 0)
tmpCertHolder := make(map[string]SSHAuthKey)

files, err := ioutil.ReadDir(viper.GetString("authentication-keys-directory"))
if err != nil {
Expand All @@ -271,13 +339,16 @@ func loadCerts() {

parseKey := func(keyBytes []byte, fileInfo os.FileInfo) {
keyHandle := func(keyBytes []byte, fileInfo os.FileInfo) []byte {
key, _, _, rest, e := ssh.ParseAuthorizedKey(keyBytes)
key, comment, options, rest, e := ssh.ParseAuthorizedKey(keyBytes)
if e != nil {
log.Printf("Can't load file %s as public key: %s\n", fileInfo.Name(), e)
}

if key != nil {
tmpCertHolder = append(tmpCertHolder, key)

tmpCertHolder[string(key.Marshal())] = SSHAuthKey{
key, parseSSHOptions(options), comment,
}
}
return rest
}
Expand Down Expand Up @@ -318,17 +389,16 @@ func GetSSHConfig() *ssh.ServerConfig {

holderLock.Lock()
defer holderLock.Unlock()
for _, i := range certHolder {
if bytes.Equal(key.Marshal(), i.Marshal()) {
permssionsData := &ssh.Permissions{
Extensions: map[string]string{
"pubKey": string(key.Marshal()),
"pubKeyFingerprint": ssh.FingerprintSHA256(key),
},
}

return permssionsData, nil
_, ok := certHolder[string(key.Marshal())]
if ok {
permssionsData := &ssh.Permissions{
Extensions: map[string]string{
"pubKey": string(key.Marshal()),
"pubKeyFingerprint": ssh.FingerprintSHA256(key),
},
}

return permssionsData, nil
}

return nil, fmt.Errorf("public key doesn't match")
Expand Down Expand Up @@ -456,11 +526,39 @@ func GetOpenPort(addr string, port uint32, state *State, sshConn *SSHConnection)
bindPort := port
bindAddr := addr
listenAddr := ""
authPorts := make([]uint32, 0)

if bindAddr == "localhost" && viper.GetBool("localhost-as-all") {
bindAddr = ""
}

getPortsFromAuthSettings := func() []uint32 {
ret := make([]uint32, 0)
holderLock.Lock()
defer holderLock.Unlock()
if sshConn.SSHConn.Permissions == nil {
return ret
}
i, ok := certHolder[sshConn.SSHConn.Permissions.Extensions["pubKey"]]
if !ok {
return ret
}
ports, ok := i.Options["permitlisten"]
if !ok {
return ret
}

for _, p := range ports {
authPortNum, err := strconv.ParseUint(p, 10, 32)
if err != nil {
log.Println("Invalid value in permitlisten option in authorized keys:", p)
santomet marked this conversation as resolved.
Show resolved Hide resolved
continue
}
ret = append(ret, uint32(authPortNum))
}
return ret
}

reportUnavailable := func(unavailable bool) {
if first && unavailable {
extra := " Assigning a random port."
Expand All @@ -473,8 +571,19 @@ func GetOpenPort(addr string, port uint32, state *State, sshConn *SSHConnection)
}

checkPort := func(checkerAddr string, checkerPort uint32) bool {

if viper.GetBool("use-ports-from-keys") {
authPorts = getPortsFromAuthSettings()
if len(authPorts) > 0 {
if viper.GetBool("debug") {
log.Println("The host has pre-assigned ports in auth keys:", authPorts)
}
}
}

listenAddr = fmt.Sprintf("%s:%d", bindAddr, bindPort)
checkedPort, err := CheckPort(checkerPort, viper.GetString("port-bind-range"))
checkedPort, err := CheckPort(checkerPort, viper.GetString("port-bind-range"), authPorts)

_, ok := state.TCPListeners.Load(listenAddr)

if err == nil && (!viper.GetBool("tcp-load-balancer") || (viper.GetBool("tcp-load-balancer") && !ok)) {
Expand All @@ -490,7 +599,7 @@ func GetOpenPort(addr string, port uint32, state *State, sshConn *SSHConnection)
reportUnavailable(true)

if viper.GetString("port-bind-range") != "" {
bindPort = GetRandomPortInRange(viper.GetString("port-bind-range"))
bindPort = GetRandomPortInRange(viper.GetString("port-bind-range"), authPorts)
} else {
bindPort = 0
}
Expand Down