killswitch is a Linux eBPF network kill switch. The system daemon attaches an
egress program to selected interfaces, keeps per-interface policy,
and drops traffic routed to this interfaces unless the current policy
explicitly allows it (it works nice with pmark btw).
The project includes:
killswitch: system daemon that owns interface monitoring, eBPF attachment, policy evaluation, the admin API, and the optional local SOCKS proxy.killswitch-user: desktop-session helper for notifications, tray controls, network checks, and captive portal handling.killswitch-cli: command line client killswitch daemon.
Caution
killswitch is an experimental and unstable software that work under high privileges while havent being audited yet. Do not use in production.
- Fail-closed IPv4 and IPv6 egress filtering.
- Interface selection by type, exact name, and regular expression.
- Per-interface effective policies.
- Global allow rules for fwmarks, ports, destination hosts, and host+port pairs.
- Named rulesets activated by interface, address, Wi-Fi, and gateway triggers.
- Temporary connection-owned rulesets for application integrations.
- Forced ruleset activation for interactive or supervised workflows.
- Optional local SOCKS proxy whose fwmark is automatically allowed.
- Unix-socket admin API with peer credential based authorization.
- Desktop notifications, tray integration, and networks login pages handling.
Build or run directly from the flake:
nix build github:asciimoth/killswitch#killswitch
nix profile add github:asciimoth/killswitch#killswitchYou can also use system service:
imports = [ inputs.killswitch.nixosModules.killswitch ];
environment.systemPackages = [
inputs.killswitch.packages.${pkgs.system}.killswitch
];
services.killswitch = {
enable = true;
settings = {
interface_types = [ "device" ];
interface_regexps = [ "^(en|wl|ww)" ];
allow_all = false;
enable_v4 = true;
enable_v6 = true;
# allowed_ports = [ "udp/53" "tcp/53" ];
socks_proxy = {
enabled = true;
port = 1080;
fwmark = "0xeb9f0001";
dns_server = "8.8.8.8";
protected = {
usernames = [ "root" "myuser" ];
};
};
};
};Packages are published to my deb/rpm repository:
Setup it for your sytstem via script (or manually):
curl https://repo.moth.contact/setup.sh | bashThen install with your system package manager:
sudo apt install killswitch
# or
sudo dnf install killswitch
# or
sudo yum install killswitchRelease archives and package artifacts are published on the GitHub releases page.
AUR is available
By default, the daemon reads /etc/killswitch/killswitch.json. The config is a
single JSON object; unknown fields are rejected.
Example:
{
"interface_types": ["device"],
"interface_names": ["eth0", "wlan0"],
"interface_regexps": ["^(en|wl|ww)"],
"ignored_interface_types": ["bridge"],
"ignored_interface_names": ["docker0"],
"ignored_interface_regexps": ["^(veth|br-)"],
"admin_api": {
"socket_path": "/run/killswitch/admin.sock",
"auth": {
"groupnames": ["killswitch"]
}
},
"socks_proxy": {
"enabled": true,
"port": 1080,
"fwmark": "0xeb9f0001",
"dns_server": "8.8.8.8",
"protected": {
"usernames": ["root", "alice"]
}
},
"rulesets": {
"wireguard-up": {
"disabled": false,
"match": "or",
"trigger": {
"interface_names": ["wg0"],
"ip_addrs": ["10.64.0.2"]
},
"enable_v4": true,
"enable_v6": true,
"allowed_v4_hostports": ["udp/198.51.100.10:51820"]
}
},
"allow_all": false,
"enable_v4": true,
"enable_v6": true,
"allowed_marks": ["0x42"],
"allowed_ports": [],
"allowed_v4_hosts": [],
"allowed_v6_hosts": [],
"allowed_v4_hostports": ["udp/198.51.100.10:51820"],
"allowed_v6_hostports": []
}At least one of these include selectors is required:
interface_typesinterface_namesinterface_regexps
The matching ignore selectors are applied after includes:
ignored_interface_typesignored_interface_namesignored_interface_regexps
Ignored interfaces are not managed by the daemon. The loopback interface lo is
always ignored.
The top-level policy is the base policy for every selected interface:
allow_all: pass all packets before normal packet parsing. This should be a temporary recovery or debugging setting.enable_v4: enable IPv4 traffic evaluation. When false, routable IPv4 traffic is dropped except built-in bootstrap traffic.enable_v6: enable IPv6 traffic evaluation. When false, routable IPv6 traffic is dropped except built-in bootstrap traffic.allowed_marks: allow packets with matching fwmarks, for example"0x42"or"100".allowed_ports: allow TCP or UDP destination ports on any host, for example"tcp/443"or"udp/53".allowed_v4_hosts,allowed_v6_hosts: allow destination hosts regardless of port.allowed_v4_hostports,allowed_v6_hostports: allow protocol, destination host, and destination port tuples, for example"udp/198.51.100.10:51820"or"tcp/[2001:db8::10]:443".
Prefer host+port rules for fixed endpoints. allowed_ports usually is too
broad because it permits that destination port on every host.
The eBPF program allows ARP, DHCPv4, DHCPv6, and ICMPv6 Neighbor Discovery bootstrap traffic separately from these allow rules.
rulesets is an object keyed by ruleset name. A ruleset contains:
disabled: ignore the ruleset without deleting it.match:"or"or"and"."or"activates when any trigger predicate matches."and"requires every configured trigger group to match. The API also exposes this asmatch_all.trigger: activation predicates.- Policy fields using the same names as the global policy.
Supported trigger fields:
interface_typesinterface_namesinterface_regexpsip_addrsssidsbssidsgateway_macs
Rulesets activate independently per interface. For each selected interface, the daemon starts with the global policy and merges every enabled named ruleset whose trigger matches that interface. Disabled rulesets are ignored.
The daemon can run a local SOCKS proxy on 127.0.0.1. It is controlled by the
socks_proxy config block and can also be started or stopped through
killswitch-cli.
enabled: start the proxy with the daemon.port: local listen port. The default is1080.fwmark: mark applied to outbound proxy traffic. The configured mark is automatically merged intoallowed_markswhile the proxy is running.dns_server: optional DNS server used by proxied DNS handling.protected.uids,protected.gids,protected.usernames: limit which local users can connect to the proxy.
Per-user proxy access is a convenience boundary, not bulletproof protection. The listener checks the owner of incoming local connections on a best-effort basis, and that check can be subject to race conditions.
The admin API is enabled by default. It listens on a Unix socket, defaulting to
/run/killswitch/admin.sock.
Config fields:
admin_api.socket_path: absolute path to the Unix socket.admin_api.debug: enables debug notification injection for testing clients.admin_api.auth.uidsadmin_api.auth.gidsadmin_api.auth.usernamesadmin_api.auth.groupnames
If no auth rules are configured, the daemon allows members of the killswitch
group. The API uses peer credentials from the Unix socket connection.
killswitch-user should run inside the user's graphical session. It connects to
the daemon admin API and handles user-facing notifications, tray state, and
network checks.
By default, it uses:
$XDG_CONFIG_HOME/killswitch/killswitch-user.json
or:
~/.config/killswitch/killswitch-user.json
If your session starts killswitch-user before the desktop bar or D-Bus services
are ready, add a startup delay:
killswitch-user --delay 10sExample:
{
"socket_path": "/run/killswitch/admin.sock",
"notify_interface_changes": true,
"notify_global_allow_all": true,
"tray_enabled": true,
"network_check": {
"period": "300s",
"url": "http://connectivity-check.ubuntu.com/",
"status": 204,
"header": "online",
"notify": true,
"captive_portal": {
"cmd": [
"chromium",
"--proxy-server={{.ProxyAddr}}",
"--user-data-dir={{.Tmp}}",
"--no-first-run",
"--password-store=basic",
"{{.Portal}}"
]
}
}
}Config fields:
socket_path: admin API socket path.notify_interface_changes: show notifications when managed interfaces change.notify_global_allow_all: show notifications when the globalallow_allstate changes.tray_enabled: enable the system tray UI.network_check: enable periodic connectivity checks whenurlis set.
Network check fields:
period: interval such as"300s".url: HTTP endpoint to check.status: expected HTTP status code.text: optional expected response text.header: optional expected response header.timeout: optional request timeout.notify: show notifications for network check state changes.captive_portal: command launched when the network appears to require login.
The captive portal command supports template values such as {{.Portal}},
{{.ProxyAddr}}, and {{.Tmp}}. Chromium is recommended for captive portal
flows because it accepts a SOCKS proxy URL through command line arguments.
killswitch-cli talks to the daemon admin API. Every subcommand accepts
-socket PATH or -s PATH when the daemon socket is not the default.
Inspect current daemon state:
killswitch-cli get-cfg
killswitch-cli get-cfg --watch
killswitch-cli get-cfg --json-outWatch notifications:
killswitch-cli notifications
killswitch-cli notifications --json-outStart or stop the daemon SOCKS proxy:
killswitch-cli socks-proxy start
killswitch-cli socks-proxy stopMutate global policy:
killswitch-cli set -target base_policy.enable_v4 true
killswitch-cli set -target base_policy.allow_all false
killswitch-cli add -target base_policy.allowed_v4_hostports udp/198.51.100.10:51820
killswitch-cli remove -target base_policy.allowed_marks 0x42Add, change, or remove a named ruleset:
killswitch-cli add -target ruleset -ruleset wireguard-up -json '{"trigger":{"interface_names":["wg0"]},"policy":{"enable_v4":true,"allowed_v4_hostports":["udp/198.51.100.10:51820"]}}'
killswitch-cli set -target ruleset.disabled -ruleset wireguard-up true
killswitch-cli remove -target ruleset -ruleset wireguard-upInstall a temporary ruleset and keep it alive until Ctrl+C, Ctrl+D, Esc, or server disconnect:
killswitch-cli tmp-ruleset -interfaces wg0 -json '{"enable_v4":true,"allowed_v4_hostports":["udp/198.51.100.10:51820"]}'Force a named ruleset active for selected interfaces for the lifetime of the CLI connection:
killswitch-cli force-ruleset -interfaces wg0 -ruleset wireguard-upTemporary and forced rulesets are connection-scoped. They are removed automatically when the owning client disconnects.
Applications should treat killswitch bypass as an explicit user choice. Keep it disabled by default and enable it only after the user has intentionally opted in.
Recommended strategies:
- Use owned temporary rulesets for short-lived access. A temporary ruleset is tied to the admin API client connection and is automatically removed when the app exits, crashes, or disconnects.
- Prefer endpoint whitelists when the app talks to known addresses. Add host+port rules only for the required protocol, address, and port.
- Use the fwmark strategy when the app able to maek all of its traffic.
Use a temporal client owned rule with
allowed_marks.
- Privileges drop after startup in killswitch demon?
- Security audit
- killswitch-cli shell autocompletions
- Setup examples with popular VPN clients
- Tailscale?
- Amnesia
- Allow multiple urls to check network connectivity
This repository is dual licensed under GPL or MIT; see LICENSE-GPL and LICENSE-MIT.
Note: the Go bindings embed precompiled eBPF object blobs generated by bpf2go
(cmd/killswitch/killswitch_bpf*.o).
Those blobs are built from the open-source0
killswitch.c which
declare Dual MIT/GPL for the kernel verifier.
You can regenerate them with:
go generate ./...