Add per-user IP addresses

The new upstream-user-ip directive allows bouncer operators to
assign one IP address per user.
This commit is contained in:
Simon Ser 2021-10-21 19:14:39 +02:00
parent 97152191ad
commit 55840312b4
6 changed files with 94 additions and 2 deletions

View File

@ -93,6 +93,7 @@ func loadConfig() (*config.Server, *soju.Config, error) {
AcceptProxyIPs: raw.AcceptProxyIPs,
MaxUserNetworks: raw.MaxUserNetworks,
MultiUpstream: raw.MultiUpstream,
UpstreamUserIPs: raw.UpstreamUserIPs,
Debug: debug,
MOTD: motd,
}

View File

@ -52,6 +52,7 @@ type Server struct {
MaxUserNetworks int
MultiUpstream bool
UpstreamUserIPs []*net.IPNet
}
func Defaults() *Server {
@ -150,6 +151,29 @@ func parse(cfg scfg.Block) (*Server, error) {
return nil, fmt.Errorf("directive %q: %v", d.Name, err)
}
srv.MultiUpstream = v
case "upstream-user-ip":
if len(srv.UpstreamUserIPs) > 0 {
return nil, fmt.Errorf("directive %q: can only be specified once", d.Name)
}
var hasIPv4, hasIPv6 bool
for _, s := range d.Params {
_, n, err := net.ParseCIDR(s)
if err != nil {
return nil, fmt.Errorf("directive %q: failed to parse CIDR: %v", d.Name, err)
}
if n.IP.To4() == nil {
if hasIPv6 {
return nil, fmt.Errorf("directive %q: found two IPv6 CIDRs", d.Name)
}
hasIPv6 = true
} else {
if hasIPv4 {
return nil, fmt.Errorf("directive %q: found two IPv4 CIDRs", d.Name)
}
hasIPv4 = true
}
srv.UpstreamUserIPs = append(srv.UpstreamUserIPs, n)
}
default:
return nil, fmt.Errorf("unknown directive %q", d.Name)
}

View File

@ -156,6 +156,15 @@ The following directives are supported:
Globally enable or disable multi-upstream mode. By default, multi-upstream
mode is enabled.
*upstream-user-ip* <cidr...>
Enable per-user IP addresses. One IPv4 range and/or one IPv6 range can be
specified in CIDR notation. One IP address per range will be assigned to
each user and will be used as the source address when connecting to an
upstream network.
This can be useful to avoid having the whole bouncer banned from an upstream
network because of one malicious user.
# IRC SERVICE
soju exposes an IRC service called *BouncerServ* to manage the bouncer.

View File

@ -64,6 +64,7 @@ type Config struct {
MaxUserNetworks int
MultiUpstream bool
MOTD string
UpstreamUserIPs []*net.IPNet
}
type Server struct {

View File

@ -140,6 +140,11 @@ func connectToUpstream(network *network) (*upstreamConn, error) {
addr = u.Host + ":6697"
}
dialer.LocalAddr, err = network.user.localTCPAddrForHost(host)
if err != nil {
return nil, fmt.Errorf("failed to pick local IP for remote host %q: %v", host, err)
}
logger.Printf("connecting to TLS server at address %q", addr)
tlsConfig := &tls.Config{ServerName: host, NextProtos: []string{"irc"}}
@ -174,8 +179,15 @@ func connectToUpstream(network *network) (*upstreamConn, error) {
netConn = tls.Client(netConn, tlsConfig)
case "irc+insecure":
addr := u.Host
if _, _, err := net.SplitHostPort(addr); err != nil {
addr = addr + ":6667"
host, _, err := net.SplitHostPort(addr)
if err != nil {
host = u.Host
addr = u.Host + ":6667"
}
dialer.LocalAddr, err = network.user.localTCPAddrForHost(host)
if err != nil {
return nil, fmt.Errorf("failed to pick local IP for remote host %q: %v", host, err)
}
logger.Printf("connecting to plain-text server at address %q", addr)

45
user.go
View File

@ -6,6 +6,8 @@ import (
"encoding/binary"
"encoding/hex"
"fmt"
"math/big"
"net"
"time"
"gopkg.in/irc.v3"
@ -956,3 +958,46 @@ func (u *user) hasPersistentMsgStore() bool {
_, isMem := u.msgStore.(*memoryMessageStore)
return !isMem
}
// localAddrForHost returns the local address to use when connecting to host.
// A nil address is returned when the OS should automatically pick one.
func (u *user) localTCPAddrForHost(host string) (*net.TCPAddr, error) {
upstreamUserIPs := u.srv.Config().UpstreamUserIPs
if len(upstreamUserIPs) == 0 {
return nil, nil
}
ips, err := net.LookupIP(host)
if err != nil {
return nil, err
}
wantIPv6 := false
for _, ip := range ips {
if ip.To4() == nil {
wantIPv6 = true
break
}
}
var ipNet *net.IPNet
for _, in := range upstreamUserIPs {
if wantIPv6 == (in.IP.To4() == nil) {
ipNet = in
break
}
}
if ipNet == nil {
return nil, nil
}
var ipInt big.Int
ipInt.SetBytes(ipNet.IP)
ipInt.Add(&ipInt, big.NewInt(u.ID+1))
ip := net.IP(ipInt.Bytes())
if !ipNet.Contains(ip) {
return nil, fmt.Errorf("IP network %v too small", ipNet)
}
return &net.TCPAddr{IP: ip}, nil
}