EndGame0/sourcecode/gobalance/pkg/onionbalance/manager.go
2024-10-23 20:50:14 +05:30

349 lines
13 KiB
Go

package onionbalance
import (
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"math/rand"
"os"
"regexp"
"strconv"
"time"
)
func loadDefaults() {
p := Params()
// InitialCallbackDelay How long to wait for onionbalance to bootstrap before starting periodic
// events (in nanoseconds). 10 base plus 2 seconds here for every single front you have. (10 instances = 10 + (10X2) = 30) times 1 billion.
p.SetInitialCallbackDelay(75 * 1000000000) //time is in nanoseconds
// FetchDescriptorFrequency Every how often we should be fetching instance descriptors (in seconds)
p.SetFetchDescriptorFrequency(20 * 1000000000)
// PublishDescriptorCheckFrequency Every how often we should be checking whether we should publish our frontend
// descriptor (in nanoseconds). Triggering this callback doesn't mean we will actually upload a descriptor.
// We only upload a descriptor if it has expired, the intro points have changed, etc. Default
p.SetPublishDescriptorCheckFrequency(30 * 1000000000)
// FrontendDescriptorLifetime How long should we keep a frontend descriptor before we expire it (in
// nanoseconds)?
p.SetFrontendDescriptorLifetime(40 * 1000000000)
// InstanceDescriptorTooOld If we last received a descriptor for this instance more than
// INSTANCE_DESCRIPTOR_TOO_OLD seconds ago, consider the instance to be down.
p.SetInstanceDescriptorTooOld(120 * 1000000000)
// HsdirNReplicas Number of replicas per descriptor (generally only use 2!)
p.SetHsdirNReplicas(2)
// NIntrosPerInstance How many intros should we use from each instance in the final frontend
// descriptor? Default 2 but we use 1 here.
p.SetNIntrosPerInstance(1)
// NIntrosWanted The amount of introduction points wanted for each individual descriptor
p.SetNIntrosWanted(20)
// NEWNYM is a tor control port command which clears the descriptors. Tor has a rate limit on this to about 8 seconds.
// In the event that changes this variable can be adjusted. Otherwise, don't touch.
p.SetNewnymSleep(8 * time.Second)
// Below is the adaptive configuration area. Don't touch these!
p.SetAdaptForcePublish(1)
p.SetAdaptDistinctDescriptors(1)
}
// Main This is the entry point of v3 functionality.
// Initialize onionbalance, schedule future jobs and let the scheduler do its thing.
func Main(c *cli.Context) {
loadDefaults()
p := Params()
if p.NIntrosWanted() > 20 {
logrus.Fatal("You need to reduce the NIntrosWanted param value to 20 or below. " +
"While it's possible to push more than 20 introduction points; the Tor clients, " +
"at this time, will reject the descriptor. See tor's HS_CONFIG_V3_MAX_INTRO_POINTS in hs_config.h and function " +
"desc_decode_encrypted_v3 in hs_descriptor.c")
}
p.SetAdaptEnabled(c.Bool("adaptive"))
p.SetAdaptStrict(c.Bool("strict"))
p.SetTightTimings(c.Bool("tight"))
config := c.String("config")
ip := c.String("ip")
port := c.Int("port")
quick := c.Bool("quick")
torPassword := c.String("torPassword")
start, end, err := parseRange(c.String("dirsplit"))
if err != nil {
logrus.Errorf("Your dirsplit value is invalid! Error: " + err.Error())
}
p.SetDirStart(start)
p.SetDirEnd(end)
MyOnionBalance := OnionBalance()
if err := MyOnionBalance.InitSubsystems(InitSubsystemsParams{
ConfigPath: config,
IP: ip,
Port: port,
TorPassword: torPassword,
}); err != nil {
panic(err)
}
initScheduler(quick)
}
func initScheduler(quick bool) {
p := Params()
instance := OnionBalance()
// Tell Tor to be active and ready.
go torActive(instance.Controller())
//Check if Tor has live consensus before doing anything.
if !instance.Consensus().IsLive() {
logrus.Fatal("No live consensus. Wait for Tor to grab the consensus and try again.")
}
if p.AdaptEnabled() {
adaptiveStart(*instance)
} else {
instance.FetchInstanceDescriptors()
// Quick is a hack to quickly deploy a new descriptor. Used to fix a suck descriptor.
if quick {
time.Sleep(5 * time.Second)
} else {
time.Sleep(time.Duration(p.InitialCallbackDelay()))
}
instance.PublishAllDescriptors()
}
rand.Seed(time.Now().UnixNano())
//individual async channel threads for both fetching and publishing descriptors.
go func() {
for {
select {
case <-time.After(time.Duration(p.FetchDescriptorFrequency())):
case <-p.FetchChannel:
continue
}
run := adaptFetch()
if run {
//variate timings to reduce correlation attacks
rand.Seed(time.Now().UnixNano())
millisecond := time.Duration(rand.Intn(2001)) * time.Millisecond
time.Sleep(millisecond)
instance.FetchInstanceDescriptors()
}
}
}()
go func() {
for {
select {
case <-time.After(time.Duration(p.PublishDescriptorCheckFrequency())):
case <-p.PublishChannel:
continue
}
run := adaptPublish()
if run {
//variate timings to reduce correlation attacks
rand.Seed(time.Now().UnixNano())
millisecond := time.Duration(rand.Intn(2001)) * time.Millisecond
time.Sleep(millisecond)
instance.PublishAllDescriptors()
}
}
}()
}
func torActive(instance *Controller) {
_, err := instance.Signal("ACTIVE")
if err != nil {
logrus.Panicf("Sending 'Active' signal failed. Check if your Tor control process is still alive and able to be connected to!")
}
time.Sleep(5 * time.Second)
}
func adaptiveStart(instance Onionbalance) {
p := Params()
logrus.Infof("[ADAPTIVE] Waiting for %d instance descriptors.", p.NInstances())
p.SetAdaptWgEnabled(true)
instance.FetchInstanceDescriptors()
p.AdaptWg().Wait()
//need to get the channel and see how many instances have returned within the InitialCallbackDelay time. Hoping for all of them. Warn if not.
adaptStartTime := p.AdaptStartTime()
p.SetAdaptDelay(time.Since(time.Unix(adaptStartTime, 0)).Nanoseconds())
logrus.Info("[ADAPTIVE] Adaptive Configured! It took ", p.AdaptDelay()/1000000000, " seconds to get all descriptors. Optimizing performance!")
if p.AdaptStrict() {
strictTest(p.AdaptDelay())
}
//Prevent Waitgroup Recounting. Sanity check as well.
p.SetAdaptWgEnabled(false)
logrus.Info("[ADAPTIVE] Adapting to network and instance conditions...")
//Make sure that newnym has a chance to clear descriptors
if p.AdaptDelay() < 8000000000 { //8 seconds
p.SetAdaptDelay(80000000000)
}
adaptDelay := p.AdaptDelay()
//We got all the descriptors within this timeframe so should be a good default.
p.SetFetchDescriptorFrequency(adaptDelay)
//If new descriptors are not received for 5 fetches (2 retries) count them as old.
p.SetInstanceDescriptorTooOld(adaptDelay * 5)
//Expire a descriptor after two fetches. This is not ideal for large amounts of instances.
p.SetFrontendDescriptorLifetime(adaptDelay * 2)
//Time the publishing checks with the fetch descriptors. Only publishes if needed.
p.SetPublishDescriptorCheckFrequency(adaptDelay / 2)
adaptFetch()
//adaptPublish()
//force publishing on first start
p.SetAdaptIntroChanged(1)
}
// Adaptive Publish
//
// These functions changes the way onionbalance operates to prioritize introduction rotation
// onto the network in the most ideal timings (to increase reachability). It responds to the amount of
// active instances and changes the publishing timings to the network in hopes of not overloading
// the attached Tor process. The point is to help tune in the default parameters, based on the amount
// of instances, so that it maximizes the onion service uptime. It is heavily opinionated and is not
// a perfect alternative to manual refinement.
// However, it is far better than what the original python implementation does. Aka nothing.
func adaptPublish() bool {
p := Params()
if !p.AdaptEnabled() {
return true
}
//If there is equal to or less than 20 Introduction points active disable distinct descriptors. The benefits of distinct descriptors
//are only shown with more than 20 instances. We increase reachability with tighter timings on descriptor
//pushes and better introduction point selection.
if p.NIntroduction() <= 20 {
p.SetAdaptDistinctDescriptors(1)
p.SetAdaptShuffle(1)
p := Params()
adaptDelay := p.AdaptDelay()
descriptorCheckFrequency := p.publishDescriptorCheckFrequency
//If there has been no change in any introduction point with all instances being active do not proceed.
if p.AdaptIntroChanged() == 0 && p.AdaptUp() == p.AdaptCount() {
if adaptDelay < 800000000000 { //800 seconds max limit change
logrus.Info("[ADAPTIVE] Slowing down descriptor publishing")
p.SetPublishDescriptorCheckFrequency(adaptDelay * 2)
}
logrus.Info("[ADAPTIVE] Skipping descriptor push as there has been no introduction point change.")
return false
} else {
if adaptDelay == descriptorCheckFrequency {
logrus.Info("[ADAPTIVE] Speeding up descriptor publishing")
p.SetPublishDescriptorCheckFrequency(adaptDelay / 2)
}
}
} else {
p.SetAdaptDistinctDescriptors(1)
//If the number of introduction points are less than the amount it takes to fill all HSDIR descriptors,
//configure the push of introduction points to prioritize the freshest descriptors received. Otherwise, treat all
//introduction points as equal priority.
maxintropoints := p.AdaptHSDirCount() * 20
if maxintropoints > p.NIntroduction() {
p.SetAdaptShuffle(1)
} else {
p.SetAdaptShuffle(0)
}
}
//ADAPT TIMING ADJUSTMENTS REMOVED (correlation attack potential)
return true
}
// Adaptive Fetching
func adaptFetch() bool {
p := Params()
if !p.AdaptEnabled() {
return true
}
//warn if some instances are down
if p.AdaptCount() != p.NInstances() {
if p.AdaptDownNoDescriptor() != 0 {
logrus.Infof("[ADAPTIVE] There are %d instances who have no returned Descriptors. If you see this message a lot "+
"stop gobalance and remove the offline instances for better performance.", p.AdaptDownNoDescriptor())
}
if p.AdaptDownInstanceOld() != 0 {
logrus.Infof("[ADAPTIVE] There are %d instances who have old descriptors. If you see this message a lot "+
"stop gobalance, reset tor, and remove the offline instances for better performance.", p.AdaptDownInstanceOld())
}
}
//ADAPT TIMING ADJUSTMENTS REMOVED. (correlation attack potential)
return true
}
// strictTest Returns if adaptive timings are reasonable giving the number of instances. Runs only on start.
// Gracefully exits on failure. Configurable with the "strict" cli option. Defaults to true.
func strictTest(timings int64) {
p := Params()
//Check if there are failed services within the config.yaml which returned with no descriptors exit with warning.
//Best to clear out the downed instances or wait for their recovery before doing anything else
nInstances := p.NInstances()
if nInstances < p.AdaptCount() {
logrus.Infof("[STRICT] Some instances are down at start of this process. Wait for their recovery or remove " +
"the downed instances.")
if logrus.GetLevel().String() != "debug" {
logrus.Infof("[STRICT] Set '--verbosity bebug' to see downed instances.")
}
os.Exit(0)
}
//Tor has a soft max of 32 general-purpose client circuits that can be pending.
//If you go over that value it will wait until some finish. This means having more than 32 fronts will greatly limit
//your circuit builds.
if nInstances > 32 {
logrus.Infof("[STRICT] You have over 32 active fronts. Tor has a soft limit of 32 general-purpose pending circuits." +
"For the best performance split your fronts and descriptor push over multiple gobalance instances and Tor processes")
logrus.Debugf("You have %d active fronts. You want under 32.", nInstances)
os.Exit(0)
}
//From many tests the tolerance for a series of instances on a single Tor process is
//a simple base of 10 plus 3 per instance. It takes in account the delay of the Tor network circuit building
//The timings here was calculated when the Tor network was under DDOS with extreme latency build issues.
//This will be probably inaccurate in times of peace and should be tightened further.
maxTimings := (10 + (5 * nInstances)) * 1000000000
if maxTimings < timings {
logrus.Infof("[STRICT] The Tor process is too slow to handle %d instances in current network conditions. "+
"Reduce the amount of instances on an individual onionbalance and tor process to pass strict test checks or "+
"disable with cli --strict false.", nInstances)
logrus.Debugf("strictTimings=%d and reported timings=%d in seconds", maxTimings/1000000000, timings/1000000000)
os.Exit(0)
}
}
func parseRange(input string) (int, int, error) {
if input == "" {
return 0, 0, nil
}
pattern := `^([1-8])(?:-([1-8]))?$`
re := regexp.MustCompile(pattern)
matches := re.FindStringSubmatch(input)
if matches == nil {
logrus.Errorf("The dirsplit value is invalid. You need to have it within the range of 1-8!")
}
start, err := strconv.Atoi(matches[1])
if err != nil {
return 0, 0, err
}
end := start
if matches[2] != "" {
end, err = strconv.Atoi(matches[2])
if err != nil {
return 0, 0, err
}
}
// Make sure the end is greater than or equal to the start
if end < start {
logrus.Errorf("End number should be greater than or equal to the start number. It's a assending range!")
}
return start, end, nil
}