mirror of
https://github.com/ergochat/ergo.git
synced 2024-12-22 10:42:52 +01:00
commit
13dda00989
1
Makefile
1
Makefile
@ -20,6 +20,7 @@ test:
|
||||
python3 ./gencapdefs.py | diff - ${capdef_file}
|
||||
cd irc && go test . && go vet .
|
||||
cd irc/caps && go test . && go vet .
|
||||
cd irc/cloaks && go test . && go vet .
|
||||
cd irc/connection_limits && go test . && go vet .
|
||||
cd irc/history && go test . && go vet .
|
||||
cd irc/isupport && go test . && go vet .
|
||||
|
@ -26,6 +26,7 @@ _Copyright © 2018 Daniel Oaks <daniel@danieloaks.net>_
|
||||
- Nickname reservation
|
||||
- Channel Registration
|
||||
- Language
|
||||
- IP cloaking
|
||||
- Frequently Asked Questions
|
||||
- Modes
|
||||
- User Modes
|
||||
@ -242,6 +243,16 @@ The above will change the server language to Romanian, with a fallback to Chines
|
||||
Our language and translation functionality is very early, so feel free to let us know if there are any troubles with it! If you know another language and you'd like to contribute, we've got a CrowdIn project here: [https://crowdin.com/project/oragono](https://crowdin.com/project/oragono)
|
||||
|
||||
|
||||
## IP cloaking
|
||||
|
||||
Unlike many other chat and web platforms, IRC traditionally exposes the user's IP and hostname information to other users. This is in part because channel owners and operators (who have privileges over a single channel, but not over the server as a whole) need to be able to ban spammers and abusers from their channels, including via hostnames in cases where the abuser tries to evade the ban.
|
||||
|
||||
IP cloaking is a way of balancing these concerns about abuse with concerns about user privacy. With cloaking, the user's IP address is deterministically "scrambled", typically via a cryptographic [MAC](https://en.wikipedia.org/wiki/Message_authentication_code), to form a "cloaked" hostname that replaces the usual reverse-DNS-based hostname. Users cannot reverse the scrambling to learn each other's IPs, but can ban a scrambled address the same way they would ban a regular hostname.
|
||||
|
||||
Oragono supports cloaking, which can be enabled via the `server.ip-cloaking` section of the config. However, Oragono's cloaking behavior differs from other IRC software. Rather than scrambling each of the 4 bytes of the IPv4 address (or each 2-byte pair of the 8 such pairs of the IPv6 address) separately, the server administrator configures a CIDR length (essentially, a fixed number of most-significant-bits of the address). The CIDR (i.e., only the most significant portion of the address) is then scrambled atomically to produce the cloaked hostname. This errs on the side of user privacy, since knowing the cloaked hostname for one CIDR tells you nothing about the cloaked hostnames of other CIDRs --- the scheme reveals only whether two users are coming from the same CIDR. We suggest using 32-bit CIDRs for IPv4 (i.e., the whole address) and 64-bit CIDRs for IPv6, since these are the typical assignments made by ISPs to individual customers.
|
||||
|
||||
Setting `server.ip-cloaking.num-bits` to 0 gives users cloaks that don't depend on their IP address information at all, which is an option for deployments where privacy is a more pressing concern than abuse. Holders of registered accounts can also use the vhost system (for details, `/msg HostServ HELP`.)
|
||||
|
||||
-------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
@ -70,6 +70,7 @@ type Client struct {
|
||||
preregNick string
|
||||
proxiedIP net.IP // actual remote IP if using the PROXY protocol
|
||||
rawHostname string
|
||||
cloakedHostname string
|
||||
realname string
|
||||
realIP net.IP
|
||||
registered bool
|
||||
@ -215,6 +216,7 @@ func RunNewClient(server *Server, conn clientConn) {
|
||||
session.realIP = utils.AddrToIP(remoteAddr)
|
||||
// set the hostname for this client (may be overridden later by PROXY or WEBIRC)
|
||||
session.rawHostname = utils.LookupHostname(session.realIP.String())
|
||||
client.cloakedHostname = config.Server.Cloaks.ComputeCloak(session.realIP)
|
||||
if utils.AddrIsLocal(remoteAddr) {
|
||||
// treat local connections as secure (may be overridden later by WEBIRC)
|
||||
client.SetMode(modes.TLS, true)
|
||||
@ -812,7 +814,10 @@ func (client *Client) updateNick(nick, nickCasefolded, skeleton string) {
|
||||
func (client *Client) updateNickMaskNoMutex() {
|
||||
client.hostname = client.getVHostNoMutex()
|
||||
if client.hostname == "" {
|
||||
client.hostname = client.rawHostname
|
||||
client.hostname = client.cloakedHostname
|
||||
if client.hostname == "" {
|
||||
client.hostname = client.rawHostname
|
||||
}
|
||||
}
|
||||
|
||||
cfhostname, err := Casefold(client.hostname)
|
||||
@ -831,6 +836,7 @@ func (client *Client) AllNickmasks() (masks []string) {
|
||||
nick := client.nickCasefolded
|
||||
username := client.username
|
||||
rawHostname := client.rawHostname
|
||||
cloakedHostname := client.cloakedHostname
|
||||
vhost := client.getVHostNoMutex()
|
||||
client.stateMutex.RUnlock()
|
||||
username = strings.ToLower(username)
|
||||
@ -849,6 +855,10 @@ func (client *Client) AllNickmasks() (masks []string) {
|
||||
masks = append(masks, rawhostmask)
|
||||
}
|
||||
|
||||
if cloakedHostname != "" {
|
||||
masks = append(masks, fmt.Sprintf("%s!%s@%s", nick, username, cloakedHostname))
|
||||
}
|
||||
|
||||
ipmask := fmt.Sprintf("%s!%s@%s", nick, username, client.IPString())
|
||||
if ipmask != rawhostmask {
|
||||
masks = append(masks, ipmask)
|
||||
|
106
irc/cloaks/cloak_test.go
Normal file
106
irc/cloaks/cloak_test.go
Normal file
@ -0,0 +1,106 @@
|
||||
// Copyright (c) 2019 Shivaram Lingamneni
|
||||
// released under the MIT license
|
||||
|
||||
package cloaks
|
||||
|
||||
import (
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func assertEqual(supplied, expected interface{}, t *testing.T) {
|
||||
if !reflect.DeepEqual(supplied, expected) {
|
||||
t.Errorf("expected %v but got %v", expected, supplied)
|
||||
}
|
||||
}
|
||||
|
||||
func easyParseIP(ipstr string) (result net.IP) {
|
||||
result = net.ParseIP(ipstr)
|
||||
if result == nil {
|
||||
panic(ipstr)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func cloakConfForTesting() CloakConfig {
|
||||
config := CloakConfig{
|
||||
Enabled: true,
|
||||
Netname: "oragono",
|
||||
Secret: "_BdVPWB5sray7McbFmeuJL996yaLgG4l9tEyficGXKg",
|
||||
CidrLenIPv4: 32,
|
||||
CidrLenIPv6: 64,
|
||||
NumBits: 80,
|
||||
}
|
||||
config.Initialize()
|
||||
return config
|
||||
}
|
||||
|
||||
func TestCloakDeterminism(t *testing.T) {
|
||||
config := cloakConfForTesting()
|
||||
|
||||
v4ip := easyParseIP("8.8.8.8").To4()
|
||||
assertEqual(config.ComputeCloak(v4ip), "d2z5guriqhzwazyr.oragono", t)
|
||||
// use of the 4-in-6 mapping should not affect the cloak
|
||||
v6mappedIP := v4ip.To16()
|
||||
assertEqual(config.ComputeCloak(v6mappedIP), "d2z5guriqhzwazyr.oragono", t)
|
||||
|
||||
v6ip := easyParseIP("2001:0db8::1")
|
||||
assertEqual(config.ComputeCloak(v6ip), "w7ren6nxii6f3i3d.oragono", t)
|
||||
// same CIDR, so same cloak:
|
||||
v6ipsamecidr := easyParseIP("2001:0db8::2")
|
||||
assertEqual(config.ComputeCloak(v6ipsamecidr), "w7ren6nxii6f3i3d.oragono", t)
|
||||
v6ipdifferentcidr := easyParseIP("2001:0db9::1")
|
||||
// different CIDR, different cloak:
|
||||
assertEqual(config.ComputeCloak(v6ipdifferentcidr), "ccmptyrjwsxv4f4d.oragono", t)
|
||||
|
||||
// cloak values must be sensitive to changes in the secret key
|
||||
config.Secret = "HJcXK4lLawxBE4-9SIdPji_21YiL3N5r5f5-SPNrGVY"
|
||||
assertEqual(config.ComputeCloak(v4ip), "4khy3usk8mfu42pe.oragono", t)
|
||||
assertEqual(config.ComputeCloak(v6mappedIP), "4khy3usk8mfu42pe.oragono", t)
|
||||
assertEqual(config.ComputeCloak(v6ip), "mxpk3c83vdxkek9j.oragono", t)
|
||||
assertEqual(config.ComputeCloak(v6ipsamecidr), "mxpk3c83vdxkek9j.oragono", t)
|
||||
}
|
||||
|
||||
func TestCloakShortv4Cidr(t *testing.T) {
|
||||
config := CloakConfig{
|
||||
Enabled: true,
|
||||
Netname: "oragono",
|
||||
Secret: "_BdVPWB5sray7McbFmeuJL996yaLgG4l9tEyficGXKg",
|
||||
CidrLenIPv4: 24,
|
||||
CidrLenIPv6: 64,
|
||||
NumBits: 60,
|
||||
}
|
||||
config.Initialize()
|
||||
|
||||
v4ip := easyParseIP("8.8.8.8")
|
||||
assertEqual(config.ComputeCloak(v4ip), "3cay3zc72tnui.oragono", t)
|
||||
v4ipsamecidr := easyParseIP("8.8.8.9")
|
||||
assertEqual(config.ComputeCloak(v4ipsamecidr), "3cay3zc72tnui.oragono", t)
|
||||
}
|
||||
|
||||
func TestCloakZeroBits(t *testing.T) {
|
||||
config := cloakConfForTesting()
|
||||
config.NumBits = 0
|
||||
config.Netname = "example.com"
|
||||
config.Initialize()
|
||||
|
||||
v4ip := easyParseIP("8.8.8.8").To4()
|
||||
assertEqual(config.ComputeCloak(v4ip), "example.com", t)
|
||||
}
|
||||
|
||||
func TestCloakDisabled(t *testing.T) {
|
||||
config := cloakConfForTesting()
|
||||
config.Enabled = false
|
||||
v4ip := easyParseIP("8.8.8.8").To4()
|
||||
assertEqual(config.ComputeCloak(v4ip), "", t)
|
||||
}
|
||||
|
||||
func BenchmarkCloaks(b *testing.B) {
|
||||
config := cloakConfForTesting()
|
||||
v6ip := easyParseIP("2001:0db8::1")
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
config.ComputeCloak(v6ip)
|
||||
}
|
||||
}
|
70
irc/cloaks/cloaks.go
Normal file
70
irc/cloaks/cloaks.go
Normal file
@ -0,0 +1,70 @@
|
||||
// Copyright (c) 2019 Shivaram Lingamneni
|
||||
|
||||
package cloaks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"golang.org/x/crypto/sha3"
|
||||
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
)
|
||||
|
||||
type CloakConfig struct {
|
||||
Enabled bool
|
||||
Netname string
|
||||
Secret string
|
||||
CidrLenIPv4 int `yaml:"cidr-len-ipv4"`
|
||||
CidrLenIPv6 int `yaml:"cidr-len-ipv6"`
|
||||
NumBits int `yaml:"num-bits"`
|
||||
|
||||
numBytes int
|
||||
ipv4Mask net.IPMask
|
||||
ipv6Mask net.IPMask
|
||||
}
|
||||
|
||||
func (cloakConfig *CloakConfig) Initialize() {
|
||||
// sanity checks:
|
||||
numBits := cloakConfig.NumBits
|
||||
if 0 == numBits {
|
||||
numBits = 80
|
||||
} else if 256 < numBits {
|
||||
numBits = 256
|
||||
}
|
||||
|
||||
// derived values:
|
||||
cloakConfig.numBytes = numBits / 8
|
||||
// round up to the nearest byte
|
||||
if numBits%8 != 0 {
|
||||
cloakConfig.numBytes += 1
|
||||
}
|
||||
cloakConfig.ipv4Mask = net.CIDRMask(cloakConfig.CidrLenIPv4, 32)
|
||||
cloakConfig.ipv6Mask = net.CIDRMask(cloakConfig.CidrLenIPv6, 128)
|
||||
}
|
||||
|
||||
// simple cloaking algorithm: normalize the IP to its CIDR,
|
||||
// then hash the resulting bytes with a secret key,
|
||||
// then truncate to the desired length, b32encode, and append the fake TLD.
|
||||
func (config *CloakConfig) ComputeCloak(ip net.IP) string {
|
||||
if !config.Enabled {
|
||||
return ""
|
||||
} else if config.NumBits == 0 {
|
||||
return config.Netname
|
||||
}
|
||||
var masked net.IP
|
||||
v4ip := ip.To4()
|
||||
if v4ip != nil {
|
||||
masked = v4ip.Mask(config.ipv4Mask)
|
||||
} else {
|
||||
masked = ip.Mask(config.ipv6Mask)
|
||||
}
|
||||
// SHA3(K || M):
|
||||
// https://crypto.stackexchange.com/questions/17735/is-hmac-needed-for-a-sha-3-based-mac
|
||||
input := make([]byte, len(config.Secret)+len(masked))
|
||||
copy(input, config.Secret[:])
|
||||
copy(input[len(config.Secret):], masked)
|
||||
digest := sha3.Sum512(input)
|
||||
b32digest := utils.B32Encoder.EncodeToString(digest[:config.numBytes])
|
||||
return fmt.Sprintf("%s.%s", b32digest, config.Netname)
|
||||
}
|
@ -18,6 +18,7 @@ import (
|
||||
"time"
|
||||
|
||||
"code.cloudfoundry.org/bytefmt"
|
||||
"github.com/oragono/oragono/irc/cloaks"
|
||||
"github.com/oragono/oragono/irc/connection_limits"
|
||||
"github.com/oragono/oragono/irc/custime"
|
||||
"github.com/oragono/oragono/irc/isupport"
|
||||
@ -297,6 +298,7 @@ type Config struct {
|
||||
isupport isupport.List
|
||||
ConnectionLimiter connection_limits.LimiterConfig `yaml:"connection-limits"`
|
||||
ConnectionThrottler connection_limits.ThrottlerConfig `yaml:"connection-throttling"`
|
||||
Cloaks cloaks.CloakConfig `yaml:"ip-cloaking"`
|
||||
}
|
||||
|
||||
Languages struct {
|
||||
@ -728,6 +730,13 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||
config.History.ClientLength = 0
|
||||
}
|
||||
|
||||
config.Server.Cloaks.Initialize()
|
||||
if config.Server.Cloaks.Enabled {
|
||||
if config.Server.Cloaks.Secret == "" || config.Server.Cloaks.Secret == "siaELnk6Kaeo65K3RCrwJjlWaZ-Bt3WuZ2L8MXLbNb4" {
|
||||
return nil, fmt.Errorf("You must generate a new value of server.ip-cloaking.secret to enable cloaking")
|
||||
}
|
||||
}
|
||||
|
||||
for _, listenAddress := range config.Server.TorListeners.Listeners {
|
||||
found := false
|
||||
for _, configuredListener := range config.Server.Listen {
|
||||
|
@ -70,6 +70,7 @@ func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls boo
|
||||
ipstring := parsedProxiedIP.String()
|
||||
client.server.logger.Info("localconnect-ip", "Accepted proxy IP for client", ipstring)
|
||||
rawHostname := utils.LookupHostname(ipstring)
|
||||
cloakedHostname := client.server.Config().Server.Cloaks.ComputeCloak(parsedProxiedIP)
|
||||
|
||||
client.stateMutex.Lock()
|
||||
defer client.stateMutex.Unlock()
|
||||
@ -77,6 +78,7 @@ func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls boo
|
||||
client.proxiedIP = parsedProxiedIP
|
||||
session.rawHostname = rawHostname
|
||||
client.rawHostname = rawHostname
|
||||
client.cloakedHostname = cloakedHostname
|
||||
// nickmask will be updated when the client completes registration
|
||||
// set tls info
|
||||
client.certfp = ""
|
||||
|
@ -7,11 +7,12 @@ import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base32"
|
||||
"encoding/base64"
|
||||
)
|
||||
|
||||
var (
|
||||
// slingamn's own private b32 alphabet, removing 1, l, o, and 0
|
||||
b32encoder = base32.NewEncoding("abcdefghijkmnpqrstuvwxyz23456789").WithPadding(base32.NoPadding)
|
||||
B32Encoder = base32.NewEncoding("abcdefghijkmnpqrstuvwxyz23456789").WithPadding(base32.NoPadding)
|
||||
)
|
||||
|
||||
const (
|
||||
@ -24,7 +25,7 @@ func GenerateSecretToken() string {
|
||||
var buf [16]byte
|
||||
rand.Read(buf[:])
|
||||
// 26 ASCII characters, should be fine for most purposes
|
||||
return b32encoder.EncodeToString(buf[:])
|
||||
return B32Encoder.EncodeToString(buf[:])
|
||||
}
|
||||
|
||||
// securely check if a supplied token matches a stored token
|
||||
@ -37,3 +38,10 @@ func SecretTokensMatch(storedToken string, suppliedToken string) bool {
|
||||
|
||||
return subtle.ConstantTimeCompare([]byte(storedToken), []byte(suppliedToken)) == 1
|
||||
}
|
||||
|
||||
// generate a 256-bit secret key that can be written into a config file
|
||||
func GenerateSecretKey() string {
|
||||
var buf [32]byte
|
||||
rand.Read(buf[:])
|
||||
return base64.RawURLEncoding.EncodeToString(buf[:])
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import (
|
||||
"github.com/oragono/oragono/irc"
|
||||
"github.com/oragono/oragono/irc/logger"
|
||||
"github.com/oragono/oragono/irc/mkcerts"
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
@ -46,6 +47,7 @@ Usage:
|
||||
oragono upgradedb [--conf <filename>] [--quiet]
|
||||
oragono genpasswd [--conf <filename>] [--quiet]
|
||||
oragono mkcerts [--conf <filename>] [--quiet]
|
||||
oragono mksecret [--conf <filename>] [--quiet]
|
||||
oragono run [--conf <filename>] [--quiet]
|
||||
oragono -h | --help
|
||||
oragono --version
|
||||
@ -57,7 +59,7 @@ Options:
|
||||
|
||||
arguments, _ := docopt.ParseArgs(usage, nil, version)
|
||||
|
||||
// don't require a config file for genpasswd
|
||||
// don't require a config file for genpasswd or mksecret
|
||||
if arguments["genpasswd"].(bool) {
|
||||
var password string
|
||||
fd := int(os.Stdin.Fd())
|
||||
@ -83,6 +85,9 @@ Options:
|
||||
fmt.Println()
|
||||
}
|
||||
return
|
||||
} else if arguments["mksecret"].(bool) {
|
||||
fmt.Println(utils.GenerateSecretKey())
|
||||
return
|
||||
}
|
||||
|
||||
configfile := arguments["--conf"].(string)
|
||||
|
38
oragono.yaml
38
oragono.yaml
@ -188,6 +188,44 @@ server:
|
||||
# - "192.168.1.1"
|
||||
# - "2001:0db8::/32"
|
||||
|
||||
# IP cloaking hides users' IP addresses from other users and from channel admins
|
||||
# (but not from server admins), while still allowing channel admins to ban
|
||||
# offending IP addresses or networks. In place of hostnames derived from reverse
|
||||
# DNS, users see fake domain names like pwbs2ui4377257x8.oragono. These names are
|
||||
# generated deterministically from the underlying IP address, but if the underlying
|
||||
# IP is not already known, it is infeasible to recover it from the cloaked name.
|
||||
ip-cloaking:
|
||||
# whether to enable IP cloaking
|
||||
enabled: false
|
||||
|
||||
# fake TLD at the end of the hostname, e.g., pwbs2ui4377257x8.oragono
|
||||
netname: "oragono"
|
||||
|
||||
# secret key to prevent dictionary attacks against cloaked IPs
|
||||
# any high-entropy secret is valid for this purpose:
|
||||
# you MUST generate a new one for your installation.
|
||||
# suggestion: use the output of `oragono mksecret`
|
||||
# note that rotating this key will invalidate all existing ban masks.
|
||||
secret: "siaELnk6Kaeo65K3RCrwJjlWaZ-Bt3WuZ2L8MXLbNb4"
|
||||
|
||||
# the cloaked hostname is derived only from the CIDR (most significant bits
|
||||
# of the IP address), up to a configurable number of bits. this is the
|
||||
# granularity at which bans will take effect for ipv4 (a /32 is a fully
|
||||
# specified IP address). note that changing this value will invalidate
|
||||
# any stored bans.
|
||||
cidr-len-ipv4: 32
|
||||
|
||||
# analogous value for ipv6 (an ipv6 /64 is the typical prefix assigned
|
||||
# by an ISP to an individual customer for their LAN)
|
||||
cidr-len-ipv6: 64
|
||||
|
||||
# number of bits of hash output to include in the cloaked hostname.
|
||||
# more bits means less likelihood of distinct IPs colliding,
|
||||
# at the cost of a longer cloaked hostname. if this value is set to 0,
|
||||
# all users will receive simply `netname` as their cloaked hostname.
|
||||
num-bits: 80
|
||||
|
||||
|
||||
# account options
|
||||
accounts:
|
||||
# account registration
|
||||
|
Loading…
Reference in New Issue
Block a user