Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
aba1e2b
inital push of some walt stuff (stubbed readme, main entry)
doneill612 Sep 6, 2025
9b40c4a
use cobra for cli development + cobra-cli for scaffolding
doneill612 Sep 7, 2025
ce0b00e
duplicate persistent flag removed
doneill612 Sep 7, 2025
bd42160
stub out some internals stuff for networking and elevated command exe…
doneill612 Sep 7, 2025
2ac7a12
registry definition (registry => file that tracks aliased loopback ad…
doneill612 Sep 7, 2025
6fdae78
completed registry (load, save, append, remove, clear)
doneill612 Sep 7, 2025
346898c
add, remove, and sync command implementations. however - will need to…
doneill612 Sep 7, 2025
ddca0df
atomic registry writes using tmp file + rename, similar to like a lock
doneill612 Sep 7, 2025
cf97557
merge implementation. up & down left to do.
doneill612 Sep 7, 2025
643425d
other sync methods implemented, idiomatic go fixes (no need to pass s…
doneill612 Sep 8, 2025
e04a287
missed a couple of slice pointer args - improved error logging via fmt
doneill612 Sep 8, 2025
6401de3
move OS check to root command
doneill612 Sep 8, 2025
0eb7a8c
status command
doneill612 Sep 8, 2025
6f3ab3e
clear command implemented, plus some tidying of sync (makeSets was we…
doneill612 Sep 8, 2025
5e29f9a
small abstraction of an action plan - such that sync logic can be uni…
doneill612 Sep 8, 2025
34a129f
remove up/down modes.. realizing that we should just use merge as the…
doneill612 Sep 8, 2025
467a371
refactor merge to use sync plan struct
doneill612 Sep 8, 2025
a1144ce
registry unit tests
doneill612 Sep 8, 2025
7f8dd1e
networking tests
doneill612 Sep 8, 2025
44ec21c
integration test (e2e)
doneill612 Sep 8, 2025
fcbfc9e
formatting fix, osrs = 2 fix for mode
doneill612 Sep 8, 2025
c103c74
ascii art at root
doneill612 Sep 8, 2025
784b25c
Homebrew formula
doneill612 Sep 8, 2025
d887c83
stash
doneill612 Sep 8, 2025
9075510
wrong directory cited in formula
doneill612 Sep 8, 2025
3bf01c9
rename go mod to github.com/blurite/rsprox/walt/cmd
doneill612 Sep 8, 2025
34716e7
formula update to point to main, command docs update (remove --mode f…
doneill612 Sep 8, 2025
ed77539
README updates
doneill612 Sep 8, 2025
3113f8e
relax group constraint (>=2), fix e2e test to NOT use -n flag on sudo…
doneill612 Sep 8, 2025
a0c2e7e
README update
doneill612 Sep 8, 2025
2c54000
workflow for unit tests
doneill612 Sep 8, 2025
e324cf4
forgot to change working directory
doneill612 Sep 8, 2025
6062e29
master not main
doneill612 Sep 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/walt.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: e2e
on: [push, pull_request]

jobs:
mac-e2e:
runs-on: macos-latest
defaults:
run:
working-directory: walt
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version: '1.25.1'
cache: true
cache-dependency-path: walt/go.sum # cache correct go.sum

- name: Sudo preflight
run: sudo -n -v

- name: Run tests (includes E2E when WALT_E2E=1)
env:
WALT_E2E: "1"
run: go test ./... -v
21 changes: 21 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,24 @@ bin/

### Mac OS ###
.DS_Store

### Go ###

# Ignore only build artifacts inside walt/, not the source code
/walt/walt
/walt/walt-*
/walt/*.test
/walt/*.out
/walt/*.log
/walt/*.prof
/walt/*.pprof

*.exe
*.exe~
*.dll
*.so
*.dylib

bin/
pkg/

26 changes: 26 additions & 0 deletions HomebrewFormula/walt.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
class Walt < Formula
desc "Manage loopback address aliases for RSProx on macOS"
homepage "https://github.com/blurite/rsprox"

# update these on next release - can be automated via GH action
url "https://github.com/blurite/rsprox/archive/refs/tags/v1.0.tar.gz"
sha256 "be0a466572daa88ee6308da5bafe7d2072948097536bfc35a5b1eff5e5f1550a"

license "MIT"

head "https://github.com/blurite/rsprox.git", branch: "master"

depends_on "go" => :build

def install
# The main package lives in ./walt
cd "walt" do
system "go", "build", *std_go_args(ldflags: "-s -w")
end
end

test do
out = shell_output("#{bin}/walt --help")
assert_match "Manage loopback", out
end
end
21 changes: 21 additions & 0 deletions walt/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright © 2025 David O'Neill

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
80 changes: 80 additions & 0 deletions walt/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# World Aliasing Loopback Tool (`WALT`, macOS Only)

This module contains a simple utility for macOS users that enables the RSProx loopback mechanism to function properly.

If you are **not** a macOS user — thanks for stopping by. If you are, keep reading!

## Motivation

In the current release, macOS users have to manually manage and run a shell script to alias ranges of loopback addresses:

```bash
#!/bin/bash
set -euo pipefail

# === Config ====================================================
MIN_WORLD_ID=300 # Minimum world id to whitelist, inclusive.
MAX_WORLD_ID=650 # Maximum world id to whitelist, inclusive.
GROUP_ID=2 # Proxy target (2 is Oldschool, 3 is first custom, etc).
MODE=+ # "+" to whitelist, "-" to un-whitelist
# ===============================================================

# === some sudo ifconfig stuff ... ==============================
...

echo "Alias IPs added for worlds $MIN_WORLD_ID..$MAX_WORLD_ID (group $GROUP_ID)."
```

`WALT` puts formality around this script and exposes it through a well-documented CLI with
support for add/remove, status, sync, and clear operations.

It also maintains a **registry file** which lives
at your `XDG_STATE_HOME` location if you have this environment variable set, or at `~/.local/state/walt/aliases.txt`, and tracks loopback aliases you want to have set. Upon reboot, you can sync your `lo0` network interface with the registry file (see [Usage section](#usage)).

## Requirements and Installation

Make sure you have [Xcode 16.4 or higher](https://xcodereleases.com/) and [Homebrew](https://brew.sh/) installed.

Since `WALT` is not part of the v1.0 release, you will need to tap this repo and install the development build.

```bash
brew tap blurite/rsprox https://github.com/blurite/rsprox.git
brew install --HEAD blurite/rsprox/walt
```

If `WALT` is incorporated into future releases, the `--HEAD` flag will not be required.

## Usage

**NOTE**: `walt` requires `sudo` to run - most of these commands will prompt you for your `sudo` password if you have one.

```bash
walt --help
```

#### Common commands:

```bash
# alias a single world loopback address...
walt add --min=300 --max=300 --group=2
# ... or a world range
walt add --min=255 --max=258 --group=3

# remove those aliases
walt remove --min=255 --max=258 --group=3

# sync your registry file with lo0 (useful after system reboots)
walt sync

# show all tracked loopbacks (in the registry and on lo0)
# those two should be equivalent! if they're not, use `walt sync`
walt status

# clear all aliases entirely
walt clear
```

## Reporting Issues

If you find issues or have ideas for improvement, open a PR or discussion. Bug reports
specific to Homebrew installation should include your `brew doctor` output.
47 changes: 47 additions & 0 deletions walt/cmd/add.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package cmd

import (
"fmt"

"github.com/blurite/rsprox/walt/internal"
"github.com/spf13/cobra"
)

var addCmd = &cobra.Command{
Use: "add",
Short: "Add a whitelisted loopback alias",
Run: func(cmd *cobra.Command, args []string) {
added, warnCount, errCount := 0, 0, 0
syncNeeded := false
for w := minWorld; w <= maxWorld; w++ {
ip := internal.IPForWorld(w, group)
status, err := internal.Alias(ip)
if err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "error: alias create for %s failed: %v\n", ip, err)
errCount++
continue
}
if !status {
fmt.Fprintf(cmd.ErrOrStderr(), "warn: alias %s already set\n", ip)
warnCount++
continue
}
if err := internal.Append(ip); err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "error: alias %s was added but the registry failed to update. error: %v\n", ip, err)
syncNeeded = true
errCount++
}
added++
}
fmt.Fprintf(cmd.OutOrStdout(), "Added %d aliases for %d..%d (group %d).", added, minWorld, maxWorld, group)
fmt.Fprintf(cmd.OutOrStdout(), "(%d) warnings\n", warnCount)
fmt.Fprintf(cmd.ErrOrStderr(), "(%d) errors\n", errCount)
if syncNeeded {
fmt.Fprintln(cmd.OutOrStdout(), "One or more of your errors were related to registry synchronization; Try `walt sync` to fix.")
}
},
}

func init() {
rootCmd.AddCommand(addCmd)
}
97 changes: 97 additions & 0 deletions walt/cmd/clear.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package cmd

import (
"fmt"

"github.com/blurite/rsprox/walt/internal"
"github.com/spf13/cobra"
)

var (
clearDryRun bool
clearPruneOrphan bool // remove L\R as well
)

var clearCmd = &cobra.Command{
Use: "clear",
Short: "Remove aliases and wipe the registry file",
Long: `Clear removes all aliases tracked in the registry. With --prune-orphans (default),
it also removes any live aliases not present in the registry (L\R), so the system
and registry end up in a clean state.`,
RunE: func(cmd *cobra.Command, args []string) error {
reg, err := internal.Load()
if err != nil {
return fmt.Errorf("load registry: %w", err)
}
live, err := internal.GetLiveAliases()
if err != nil {
return fmt.Errorf("fetch live loopback aliases on lo0: %w", err)
}

// This is repeated in sync command implementation... can probably migrate to a utility in the future.
regSet := make(map[string]struct{}, len(reg))
for _, ip := range reg {
regSet[ip] = struct{}{}
}
liveSet := make(map[string]struct{}, len(live))
for _, ip := range live {
liveSet[ip] = struct{}{}
}

var toRemove []string
toRemove = append(toRemove, reg...)
if clearPruneOrphan {
for _, ip := range live {
if _, ok := regSet[ip]; !ok {
toRemove = append(toRemove, ip)
}
}
}

removed, skipped, errCount := 0, 0, 0

for _, ip := range toRemove {
if clearDryRun {
fmt.Fprintf(cmd.OutOrStdout(), "[dry-run] remove %s\n", ip)
removed++
continue
}
status, err := internal.Unalias(ip)
if err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "unalias %s failed: %v\n", ip, err)
errCount++
continue
}
if status {
removed++
} else {
// not present on lo0
skipped++
}
// Also remove from registry if its still somehow in there
internal.Remove(ip)
}

if clearDryRun {
fmt.Fprintln(cmd.OutOrStdout(), "[dry-run] delete registry file")
} else if err := internal.Clear(); err != nil {
return fmt.Errorf("delete registry: %w", err)
}

fmt.Fprintf(
cmd.OutOrStdout(),
"Clear summary: removed=%d skipped=%d\n",
removed, skipped,
)
if errCount > 0 {
return fmt.Errorf("(%d) errors", errCount)
}
return nil
},
}

func init() {
rootCmd.AddCommand(clearCmd)
clearCmd.Flags().BoolVar(&clearDryRun, "dry-run", false, "preview actions without making changes")
clearCmd.Flags().BoolVar(&clearPruneOrphan, "prune-orphans", true, "also remove live aliases that are not in the registry (L\\R)")
}
53 changes: 53 additions & 0 deletions walt/cmd/remove.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package cmd

import (
"fmt"

"github.com/blurite/rsprox/walt/internal"
"github.com/spf13/cobra"
)

var removeCmd = &cobra.Command{
Use: "remove",
Short: "Remove a whitelisted loopback alias",
Run: func(cmd *cobra.Command, args []string) {
removed, warnCount, errCount := 0, 0, 0
syncNeeded := false
for w := minWorld; w <= maxWorld; w++ {
ip := internal.IPForWorld(w, group)
status, err := internal.Unalias(ip)
if err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "error: unalias %s failed: %v\n", ip, err)
errCount++
continue
}
if !status {
fmt.Fprintf(cmd.ErrOrStderr(), "warn: alias %s not currently set - nothing to remove\n", ip)
warnCount++
// still attempt to remove from registry to reduce potential drift
internal.Remove(ip)
continue
}
if err := internal.Remove(ip); err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "error: alias %s was removed but the registry failed to update - use the sync command. error: %v\n", ip, err)
errCount++
syncNeeded = true
}
removed++
}
if removed > 0 {
fmt.Fprintf(cmd.OutOrStdout(), "Successfully removed %d aliases for %d..%d (group %d)\n", removed, minWorld, maxWorld, group)
} else {
fmt.Fprintln(cmd.OutOrStdout(), "No aliases removed")
}
fmt.Fprintf(cmd.OutOrStdout(), "(%d) warnings\n", warnCount)
fmt.Fprintf(cmd.ErrOrStderr(), "(%d) errors\n", errCount)
if syncNeeded {
fmt.Fprintln(cmd.OutOrStdout(), "One or more of your errors were related to registry synchronization; Try `walt sync` to fix.")
}
},
}

func init() {
rootCmd.AddCommand(removeCmd)
}
Loading