Added IP restriction

This commit is contained in:
jebeaudet 2023-03-06 22:32:42 -05:00
parent 658a8841d7
commit 74abbe8294
7 changed files with 96 additions and 8 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
/dist
boringproxy_db.json

30
api.go
View File

@ -7,8 +7,10 @@ import (
"fmt"
"io"
"net/http"
"net/netip"
"net/url"
"strconv"
"strings"
)
type Api struct {
@ -379,6 +381,33 @@ func (a *Api) CreateTunnel(tokenData TokenData, params url.Values) (*Tunnel, err
}
}
ipRestricted := params.Get("ip-restricted") == "on"
var ips []string
if ipRestricted {
ipsAsString := params.Get("allowed-ips")
if len(ipsAsString) == 0 {
return nil, errors.New("At least one allowed IP is required")
}
ipsRaw := strings.Split(strings.ReplaceAll(ipsAsString, "\r\n", "\n"), "\n")
for _, ipAllowed := range ipsRaw {
addr, err := netip.ParseAddr(ipAllowed)
if err == nil {
prefix, err := addr.Prefix(addr.BitLen())
if err != nil {
return nil, fmt.Errorf("Invalid IP '%s' : %w", ipAllowed, err)
}
ips = append(ips, prefix.String())
} else {
_, err = netip.ParsePrefix(ipAllowed)
if err != nil {
return nil, fmt.Errorf("Invalid IP '%s' : %w", ipAllowed, err)
}
ips = append(ips, ipAllowed)
}
}
}
tlsTerm := params.Get("tls-termination")
if tlsTerm != "server" && tlsTerm != "client" && tlsTerm != "passthrough" && tlsTerm != "client-tls" && tlsTerm != "server-tls" {
return nil, errors.New("Invalid tls-termination parameter")
@ -413,6 +442,7 @@ func (a *Api) CreateTunnel(tokenData TokenData, params url.Values) (*Tunnel, err
TlsTermination: tlsTerm,
ServerAddress: sshServerAddr,
ServerPort: sshServerPort,
IPsAllowed: ips,
}
tunnel, err := a.tunMan.RequestCreateTunnel(request)

View File

@ -10,6 +10,7 @@ import (
"log"
"net"
"net/http"
"net/netip"
"os"
"strings"
"sync"
@ -352,12 +353,29 @@ func (p *Server) handleConnection(clientConn net.Conn, certConfig *certmagic.Con
clientHello, clientReader, err := peekClientHello(clientConn)
if err != nil {
log.Println("peekClientHello error", err)
clientConn.Close()
return
}
passConn := NewProxyConn(clientConn, clientReader)
tunnel, exists := p.db.GetTunnel(clientHello.ServerName)
if exists && len(tunnel.IPsAllowed) != 0 {
allowed, err := validateIpRestriction(clientConn.RemoteAddr(), &tunnel)
if err != nil {
log.Println(err.Error())
clientConn.Close()
return
}
if !allowed {
log.Printf("Closing the connection to tunnel '%s' since the IP '%s' does not match the IP restriction.\n", tunnel.Domain, clientConn.RemoteAddr().String())
err := clientConn.Close()
if err != nil {
log.Println(err.Error())
}
return
}
}
passConn := NewProxyConn(clientConn, clientReader)
if exists && (tunnel.TlsTermination == "client" || tunnel.TlsTermination == "passthrough") || tunnel.TlsTermination == "client-tls" {
p.passthroughRequest(passConn, tunnel)
@ -373,6 +391,25 @@ func (p *Server) handleConnection(clientConn net.Conn, certConfig *certmagic.Con
}
}
func validateIpRestriction(remoteAddr net.Addr, tunnel *Tunnel) (bool, error) {
addrPort, err := netip.ParseAddrPort(remoteAddr.String())
if err != nil {
return false, err
}
for _, ipAllowed := range tunnel.IPsAllowed {
network, err := netip.ParsePrefix(ipAllowed)
if err != nil {
return false, err
}
if network.Contains(addrPort.Addr()) {
return true, nil
}
}
return false, nil
}
func (p *Server) passthroughRequest(conn net.Conn, tunnel Tunnel) {
upstreamAddr := fmt.Sprintf("localhost:%d", tunnel.TunnelPort)

View File

@ -10,6 +10,7 @@ import (
"log"
"net"
"net/http"
"reflect"
"sync"
"time"
@ -237,7 +238,7 @@ func (c *Client) SyncTunnels(ctx context.Context, serverTunnels map[string]Tunne
log.Println("New tunnel", k)
c.tunnels[k] = newTun
bore = true
} else if newTun != tun {
} else if !reflect.DeepEqual(newTun, tun) {
log.Println("Restart tunnel", k)
c.cancelFuncsMutex.Lock()
c.cancelFuncs[k]()

View File

@ -56,10 +56,11 @@ type Tunnel struct {
// TODO: These are not used by clients and possibly shouldn't be
// returned in API calls.
Owner string `json:"owner"`
ClientName string `json:"client_name"`
AuthUsername string `json:"auth_username"`
AuthPassword string `json:"auth_password"`
Owner string `json:"owner"`
ClientName string `json:"client_name"`
AuthUsername string `json:"auth_username"`
AuthPassword string `json:"auth_password"`
IPsAllowed []string `json:"ips_allowed"`
}
func NewDatabase(path string) (*Database, error) {
@ -158,7 +159,7 @@ func (d *Database) AddToken(owner, client string) (string, error) {
token, err := genRandomCode(32)
if err != nil {
return "", errors.New("Could not generat token")
return "", errors.New("Could not generate token")
}
d.Tokens[token] = TokenData{

View File

@ -58,6 +58,16 @@
<input type="password" id="password" name="password">
</div>
</div>
<div class='input'>
<label for="ip-restricted">IP restriction:</label>
<input type="checkbox" id="ip-restricted" name="ip-restricted">
<div id='allowed-ips-input'>
<label for="allowed-ips">Allowed IPs (one per line):</label>
<textarea id="allowed-ips" name="allowed-ips" cols="40" rows="5"></textarea>
</div>
</div>
<div class='input'>
<label for="ssh-server-addr">Override SSH Server Address:</label>

View File

@ -215,6 +215,14 @@ main {
display: block;
}
#allowed-ips-input {
display: none;
}
#ip-restricted:checked ~ #allowed-ips-input {
display: block;
}
.token-adder {
padding: 5px;
display: flex;