From 6b47e447ae5ed88dac0695820b3558ddb84ebd4a Mon Sep 17 00:00:00 2001 From: Orr Kapel Date: Wed, 5 Nov 2025 16:41:28 +0200 Subject: [PATCH 1/5] added eviction strategy --- README.md | 1 + cmd/interactsh-server/main.go | 14 ++++++++++++++ pkg/options/server_options.go | 1 + pkg/storage/option.go | 17 +++++++++++++---- pkg/storage/storagedb.go | 9 ++++++++- 5 files changed, 37 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c3e2c9c5..9b7911ab 100644 --- a/README.md +++ b/README.md @@ -353,6 +353,7 @@ INPUT: -lip, -listen-ip string public ip address to listen on (default "0.0.0.0") -e, -eviction int number of days to persist interaction data in memory (default 30) -ne, -no-eviction disable periodic data eviction from memory + -es, -eviction-strategy string eviction strategy for interactions (sliding, fixed) (default "sliding") -a, -auth enable authentication to server using random generated token -t, -token string enable authentication to server using given token -acao-url string origin url to send in acao header to use web-client) (default "*") diff --git a/cmd/interactsh-server/main.go b/cmd/interactsh-server/main.go index 0426e95d..40481c25 100644 --- a/cmd/interactsh-server/main.go +++ b/cmd/interactsh-server/main.go @@ -48,6 +48,7 @@ func main() { flagSet.StringVarP(&cliOptions.ListenIP, "listen-ip", "lip", "0.0.0.0", "public ip address to listen on"), flagSet.IntVarP(&cliOptions.Eviction, "eviction", "e", 30, "number of days to persist interaction data in memory"), flagSet.BoolVarP(&cliOptions.NoEviction, "no-eviction", "ne", false, "disable periodic data eviction from memory"), + flagSet.StringVarP(&cliOptions.EvictionStrategy, "eviction-strategy", "es", "sliding", "eviction strategy for interactions (sliding, fixed)"), flagSet.BoolVarP(&cliOptions.Auth, "auth", "a", false, "enable authentication to server using random generated token"), flagSet.StringVarP(&cliOptions.Token, "token", "t", "", "enable authentication to server using given token"), flagSet.StringVar(&cliOptions.OriginURL, "acao-url", "*", "origin url to send in acao header to use web-client)"), // cli flag set to deprecate @@ -218,9 +219,22 @@ func main() { if cliOptions.NoEviction { evictionTTL = -1 } + + // Parse eviction strategy + var evictionStrategy storage.EvictionStrategy + switch strings.ToLower(cliOptions.EvictionStrategy) { + case "fixed": + evictionStrategy = storage.EvictionStrategyFixed + case "sliding": + evictionStrategy = storage.EvictionStrategySliding + default: + gologger.Fatal().Msgf("invalid eviction strategy '%s', must be 'sliding' or 'fixed'\n", cliOptions.EvictionStrategy) + } + var store storage.Storage storeOptions := storage.DefaultOptions storeOptions.EvictionTTL = evictionTTL + storeOptions.EvictionStrategy = evictionStrategy if cliOptions.DiskStorage { if cliOptions.DiskStoragePath == "" { gologger.Fatal().Msgf("disk storage path must be specified\n") diff --git a/pkg/options/server_options.go b/pkg/options/server_options.go index 3001427e..6e05b97f 100644 --- a/pkg/options/server_options.go +++ b/pkg/options/server_options.go @@ -20,6 +20,7 @@ type CLIServerOptions struct { LdapWithFullLogger bool Eviction int NoEviction bool + EvictionStrategy string Responder bool Smb bool SmbPort int diff --git a/pkg/storage/option.go b/pkg/storage/option.go index ce827b8b..04bd0f18 100644 --- a/pkg/storage/option.go +++ b/pkg/storage/option.go @@ -2,10 +2,18 @@ package storage import "time" +type EvictionStrategy int + +const ( + EvictionStrategySliding EvictionStrategy = iota // expire-after-access + EvictionStrategyFixed // expire-after-write +) + type Options struct { - DbPath string - EvictionTTL time.Duration - MaxSize int + DbPath string + EvictionTTL time.Duration + MaxSize int + EvictionStrategy EvictionStrategy } func (options *Options) UseDisk() bool { @@ -13,5 +21,6 @@ func (options *Options) UseDisk() bool { } var DefaultOptions = Options{ - MaxSize: 2500000, + MaxSize: 2500000, + EvictionStrategy: EvictionStrategySliding, } diff --git a/pkg/storage/storagedb.go b/pkg/storage/storagedb.go index 55e6df25..50e3c8b4 100644 --- a/pkg/storage/storagedb.go +++ b/pkg/storage/storagedb.go @@ -38,7 +38,14 @@ func New(options *Options) (*StorageDB, error) { cache.WithMaximumSize(options.MaxSize), } if options.EvictionTTL > 0 { - cacheOptions = append(cacheOptions, cache.WithExpireAfterAccess(options.EvictionTTL)) + switch options.EvictionStrategy { + case EvictionStrategyFixed: + cacheOptions = append(cacheOptions, cache.WithExpireAfterWrite(options.EvictionTTL)) + case EvictionStrategySliding: + fallthrough + default: + cacheOptions = append(cacheOptions, cache.WithExpireAfterAccess(options.EvictionTTL)) + } } if options.UseDisk() { cacheOptions = append(cacheOptions, cache.WithRemovalListener(storageDB.OnCacheRemovalCallback)) From 2f2071bce67911bf7bb2c9560bbad88109521d16 Mon Sep 17 00:00:00 2001 From: Orr Kapel Date: Mon, 10 Nov 2025 15:58:02 +0200 Subject: [PATCH 2/5] added tests for eviction strategy --- pkg/storage/storagedb_test.go | 46 +++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/pkg/storage/storagedb_test.go b/pkg/storage/storagedb_test.go index 0d91886b..314fc889 100644 --- a/pkg/storage/storagedb_test.go +++ b/pkg/storage/storagedb_test.go @@ -133,3 +133,49 @@ func doStuffWithOtherCache(cache cache.Cache) { _, _ = cache.GetIfPresent(strconv.Itoa(i)) } } + +func TestSlidingEvictionStrategy(t *testing.T) { + testTTL := 100 * time.Millisecond + smallDelay := 10 * time.Millisecond + mem, err := New(&Options{EvictionTTL: testTTL, EvictionStrategy: EvictionStrategySliding}) + require.Nil(t, err) + defer mem.Close() + + err = mem.SetID("test-sliding") + require.Nil(t, err) + + // Access after half TTL - should extend expiration + time.Sleep(testTTL / 2) + _, ok := mem.cache.GetIfPresent("test-sliding") + require.True(t, ok) + + // Still present after original TTL due to sliding window + time.Sleep(testTTL / 2 + smallDelay) + _, ok = mem.cache.GetIfPresent("test-sliding") + require.True(t, ok) + + // Should be expired after full TTL despite access + time.Sleep(testTTL + smallDelay) + _, ok = mem.cache.GetIfPresent("test-sliding") + require.False(t, ok) +} + +func TestFixedEvictionStrategy(t *testing.T) { + testTTL := 100 * time.Millisecond + mem, err := New(&Options{EvictionTTL: testTTL, EvictionStrategy: EvictionStrategyFixed}) + require.Nil(t, err) + defer mem.Close() + + err = mem.SetID("test-fixed") + require.Nil(t, err) + + // Access after half TTL - should NOT extend expiration + time.Sleep(testTTL / 2) + _, ok := mem.cache.GetIfPresent("test-fixed") + require.True(t, ok) + + // Should be expired after full TTL despite access + time.Sleep(testTTL / 2 + 10 * time.Millisecond) + _, ok = mem.cache.GetIfPresent("test-fixed") + require.False(t, ok) +} From e1332cebf0185917a8570d19cfda5d026eeb44f6 Mon Sep 17 00:00:00 2001 From: Mzack9999 Date: Mon, 24 Nov 2025 15:10:59 +0400 Subject: [PATCH 3/5] Revert "feat(server) added eviction strategy" --- README.md | 1 - cmd/interactsh-server/main.go | 14 ----------- pkg/options/server_options.go | 1 - pkg/storage/option.go | 17 +++---------- pkg/storage/storagedb.go | 9 +------ pkg/storage/storagedb_test.go | 46 ----------------------------------- 6 files changed, 5 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index 9b7911ab..c3e2c9c5 100644 --- a/README.md +++ b/README.md @@ -353,7 +353,6 @@ INPUT: -lip, -listen-ip string public ip address to listen on (default "0.0.0.0") -e, -eviction int number of days to persist interaction data in memory (default 30) -ne, -no-eviction disable periodic data eviction from memory - -es, -eviction-strategy string eviction strategy for interactions (sliding, fixed) (default "sliding") -a, -auth enable authentication to server using random generated token -t, -token string enable authentication to server using given token -acao-url string origin url to send in acao header to use web-client) (default "*") diff --git a/cmd/interactsh-server/main.go b/cmd/interactsh-server/main.go index 40481c25..0426e95d 100644 --- a/cmd/interactsh-server/main.go +++ b/cmd/interactsh-server/main.go @@ -48,7 +48,6 @@ func main() { flagSet.StringVarP(&cliOptions.ListenIP, "listen-ip", "lip", "0.0.0.0", "public ip address to listen on"), flagSet.IntVarP(&cliOptions.Eviction, "eviction", "e", 30, "number of days to persist interaction data in memory"), flagSet.BoolVarP(&cliOptions.NoEviction, "no-eviction", "ne", false, "disable periodic data eviction from memory"), - flagSet.StringVarP(&cliOptions.EvictionStrategy, "eviction-strategy", "es", "sliding", "eviction strategy for interactions (sliding, fixed)"), flagSet.BoolVarP(&cliOptions.Auth, "auth", "a", false, "enable authentication to server using random generated token"), flagSet.StringVarP(&cliOptions.Token, "token", "t", "", "enable authentication to server using given token"), flagSet.StringVar(&cliOptions.OriginURL, "acao-url", "*", "origin url to send in acao header to use web-client)"), // cli flag set to deprecate @@ -219,22 +218,9 @@ func main() { if cliOptions.NoEviction { evictionTTL = -1 } - - // Parse eviction strategy - var evictionStrategy storage.EvictionStrategy - switch strings.ToLower(cliOptions.EvictionStrategy) { - case "fixed": - evictionStrategy = storage.EvictionStrategyFixed - case "sliding": - evictionStrategy = storage.EvictionStrategySliding - default: - gologger.Fatal().Msgf("invalid eviction strategy '%s', must be 'sliding' or 'fixed'\n", cliOptions.EvictionStrategy) - } - var store storage.Storage storeOptions := storage.DefaultOptions storeOptions.EvictionTTL = evictionTTL - storeOptions.EvictionStrategy = evictionStrategy if cliOptions.DiskStorage { if cliOptions.DiskStoragePath == "" { gologger.Fatal().Msgf("disk storage path must be specified\n") diff --git a/pkg/options/server_options.go b/pkg/options/server_options.go index 6e05b97f..3001427e 100644 --- a/pkg/options/server_options.go +++ b/pkg/options/server_options.go @@ -20,7 +20,6 @@ type CLIServerOptions struct { LdapWithFullLogger bool Eviction int NoEviction bool - EvictionStrategy string Responder bool Smb bool SmbPort int diff --git a/pkg/storage/option.go b/pkg/storage/option.go index 04bd0f18..ce827b8b 100644 --- a/pkg/storage/option.go +++ b/pkg/storage/option.go @@ -2,18 +2,10 @@ package storage import "time" -type EvictionStrategy int - -const ( - EvictionStrategySliding EvictionStrategy = iota // expire-after-access - EvictionStrategyFixed // expire-after-write -) - type Options struct { - DbPath string - EvictionTTL time.Duration - MaxSize int - EvictionStrategy EvictionStrategy + DbPath string + EvictionTTL time.Duration + MaxSize int } func (options *Options) UseDisk() bool { @@ -21,6 +13,5 @@ func (options *Options) UseDisk() bool { } var DefaultOptions = Options{ - MaxSize: 2500000, - EvictionStrategy: EvictionStrategySliding, + MaxSize: 2500000, } diff --git a/pkg/storage/storagedb.go b/pkg/storage/storagedb.go index 50e3c8b4..55e6df25 100644 --- a/pkg/storage/storagedb.go +++ b/pkg/storage/storagedb.go @@ -38,14 +38,7 @@ func New(options *Options) (*StorageDB, error) { cache.WithMaximumSize(options.MaxSize), } if options.EvictionTTL > 0 { - switch options.EvictionStrategy { - case EvictionStrategyFixed: - cacheOptions = append(cacheOptions, cache.WithExpireAfterWrite(options.EvictionTTL)) - case EvictionStrategySliding: - fallthrough - default: - cacheOptions = append(cacheOptions, cache.WithExpireAfterAccess(options.EvictionTTL)) - } + cacheOptions = append(cacheOptions, cache.WithExpireAfterAccess(options.EvictionTTL)) } if options.UseDisk() { cacheOptions = append(cacheOptions, cache.WithRemovalListener(storageDB.OnCacheRemovalCallback)) diff --git a/pkg/storage/storagedb_test.go b/pkg/storage/storagedb_test.go index 314fc889..0d91886b 100644 --- a/pkg/storage/storagedb_test.go +++ b/pkg/storage/storagedb_test.go @@ -133,49 +133,3 @@ func doStuffWithOtherCache(cache cache.Cache) { _, _ = cache.GetIfPresent(strconv.Itoa(i)) } } - -func TestSlidingEvictionStrategy(t *testing.T) { - testTTL := 100 * time.Millisecond - smallDelay := 10 * time.Millisecond - mem, err := New(&Options{EvictionTTL: testTTL, EvictionStrategy: EvictionStrategySliding}) - require.Nil(t, err) - defer mem.Close() - - err = mem.SetID("test-sliding") - require.Nil(t, err) - - // Access after half TTL - should extend expiration - time.Sleep(testTTL / 2) - _, ok := mem.cache.GetIfPresent("test-sliding") - require.True(t, ok) - - // Still present after original TTL due to sliding window - time.Sleep(testTTL / 2 + smallDelay) - _, ok = mem.cache.GetIfPresent("test-sliding") - require.True(t, ok) - - // Should be expired after full TTL despite access - time.Sleep(testTTL + smallDelay) - _, ok = mem.cache.GetIfPresent("test-sliding") - require.False(t, ok) -} - -func TestFixedEvictionStrategy(t *testing.T) { - testTTL := 100 * time.Millisecond - mem, err := New(&Options{EvictionTTL: testTTL, EvictionStrategy: EvictionStrategyFixed}) - require.Nil(t, err) - defer mem.Close() - - err = mem.SetID("test-fixed") - require.Nil(t, err) - - // Access after half TTL - should NOT extend expiration - time.Sleep(testTTL / 2) - _, ok := mem.cache.GetIfPresent("test-fixed") - require.True(t, ok) - - // Should be expired after full TTL despite access - time.Sleep(testTTL / 2 + 10 * time.Millisecond) - _, ok = mem.cache.GetIfPresent("test-fixed") - require.False(t, ok) -} From b9eba1d42c2dc82d56342d92777f43655d065430 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 18:22:43 +0000 Subject: [PATCH 4/5] chore(deps): bump github.com/refraction-networking/utls Bumps [github.com/refraction-networking/utls](https://github.com/refraction-networking/utls) from 1.8.0 to 1.8.2. - [Release notes](https://github.com/refraction-networking/utls/releases) - [Commits](https://github.com/refraction-networking/utls/compare/v1.8.0...v1.8.2) --- updated-dependencies: - dependency-name: github.com/refraction-networking/utls dependency-version: 1.8.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 41332ffc..6afa639d 100644 --- a/go.mod +++ b/go.mod @@ -107,7 +107,7 @@ require ( github.com/projectdiscovery/machineid v0.0.0-20250715113114-c77eb3567582 // indirect github.com/projectdiscovery/mapcidr v1.1.97 // indirect github.com/projectdiscovery/networkpolicy v0.1.34 // indirect - github.com/refraction-networking/utls v1.8.0 // indirect + github.com/refraction-networking/utls v1.8.2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect github.com/shirou/gopsutil/v3 v3.24.5 // indirect diff --git a/go.sum b/go.sum index c510b2fb..621a2148 100644 --- a/go.sum +++ b/go.sum @@ -311,8 +311,8 @@ github.com/projectdiscovery/retryablehttp-go v1.3.5/go.mod h1:2ma5Itx44tgfZCtHqn github.com/projectdiscovery/utils v0.9.0 h1:eu9vdbP0VYXI9nGSLfnOpUqBeW9/B/iSli7U8gPKZw8= github.com/projectdiscovery/utils v0.9.0/go.mod h1:zcVu1QTlMi5763qCol/L3ROnbd/UPSBP8fI5PmcnF6s= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/refraction-networking/utls v1.8.0 h1:L38krhiTAyj9EeiQQa2sg+hYb4qwLCqdMcpZrRfbONE= -github.com/refraction-networking/utls v1.8.0/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= +github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo= +github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/remeh/sizedwaitgroup v1.0.0 h1:VNGGFwNo/R5+MJBf6yrsr110p0m4/OX4S3DCy7Kyl5E= github.com/remeh/sizedwaitgroup v1.0.0/go.mod h1:3j2R4OIe/SeS6YDhICBy22RWjJC5eNCJ1V+9+NVNYlo= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= From 38bd467d95716f37569c7f17fd09fac4602713f6 Mon Sep 17 00:00:00 2001 From: XiKi-Home Date: Thu, 5 Mar 2026 19:43:01 +0100 Subject: [PATCH 5/5] fix(storage): correct OnCacheRemovalCallback to actually delete LevelDB entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs in OnCacheRemovalCallback prevented evicted correlation IDs from being removed from LevelDB: Bug 1 — wrong type assertion The callback received (key cache.Key, value cache.Value). The code did 'if key, ok := value.([]byte)' — asserting the *value* as []byte. Cache values are stored as *CorrelationData, so this assertion always fails and the db.Delete call was unreachable. Bug 2 — wrong variable shadowing Even if the assertion had succeeded, the shadow variable 'key' would hold the value bytes, not the correlation-ID string, so the wrong key would have been deleted. Effect: when a correlation ID was evicted from the in-memory cache (e.g. after TTL expiry), its LevelDB row was never cleaned up. If the client then re-registered the same correlation ID, a fresh AES key was generated and stored in the cache, but the old LevelDB row — encrypted with the previous AES key — was returned alongside new interactions. Decryption with the new key produced corrupted JSON, causing the unmarshal error: 'server.Interaction.Protocol: ReadString: invalid control character found' Fix: assert key.(string) (correlation IDs are stored as plain strings), guard with UseDisk(), then delete the correct row. Fixes #1340 --- pkg/storage/storagedb.go | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/pkg/storage/storagedb.go b/pkg/storage/storagedb.go index 019f9822..d29a595c 100644 --- a/pkg/storage/storagedb.go +++ b/pkg/storage/storagedb.go @@ -73,10 +73,26 @@ func New(options *Options) (*StorageDB, error) { return storageDB, nil } +// OnCacheRemovalCallback is called by the in-memory cache when a correlation +// ID entry is evicted (e.g. TTL expiry). It removes the corresponding +// LevelDB entry so that stale encrypted data from the old AES key is not +// returned to a client that later re-registers the same correlation ID with +// a new key. +// +// The previous implementation had two bugs: +// 1. It asserted `value.([]byte)`, but the cache stores `*CorrelationData`, +// so the assertion always failed and the LevelDB entry was never deleted. +// 2. Even if the assertion succeeded, `key` (the shadow variable) would be +// the value bytes, not the correlation-ID string — deleting the wrong key. func (s *StorageDB) OnCacheRemovalCallback(key cache.Key, value cache.Value) { - if key, ok := value.([]byte); ok { - _ = s.db.Delete(key, &opt.WriteOptions{}) + if !s.Options.UseDisk() { + return } + correlationID, ok := key.(string) + if !ok { + return + } + _ = s.db.Delete([]byte(correlationID), &opt.WriteOptions{}) } func (s *StorageDB) GetCacheMetrics() (*CacheMetrics, error) {