wol-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
wol_cli/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ """Wake-on-LAN magic packet sender with VLAN support."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from .packet import build_magic_packet, parse_mac
6
+ from .transport import broadcast_from_network, send_dot1q, send_udp
7
+
8
+ __all__ = [
9
+ "__version__",
10
+ "build_magic_packet",
11
+ "parse_mac",
12
+ "send_udp",
13
+ "send_dot1q",
14
+ "broadcast_from_network",
15
+ ]
wol_cli/__main__.py ADDED
@@ -0,0 +1,8 @@
1
+ """Entry point for python -m wol_cli."""
2
+
3
+ import sys
4
+
5
+ from .cli import main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(main())
wol_cli/cli.py ADDED
@@ -0,0 +1,92 @@
1
+ """Command-line interface for Wake-on-LAN tool."""
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ from . import __version__
7
+ from .transport import broadcast_from_network, send_dot1q, send_udp
8
+
9
+
10
+ def create_parser() -> argparse.ArgumentParser:
11
+ """Create the argument parser."""
12
+ parser = argparse.ArgumentParser(
13
+ prog="wol",
14
+ description="Send a Wake-on-LAN magic packet, with optional VLAN support.",
15
+ formatter_class=argparse.RawDescriptionHelpFormatter,
16
+ epilog="""
17
+ Examples:
18
+ # Plain UDP broadcast (same subnet)
19
+ wol AA:BB:CC:DD:EE:FF
20
+
21
+ # Directed broadcast - auto-computes 192.168.10.255
22
+ wol AA:BB:CC:DD:EE:FF --network 192.168.10.0/24
23
+
24
+ # 802.1Q tagged raw frame (Linux, must be root)
25
+ sudo wol AA:BB:CC:DD:EE:FF --vlan-id 10 --interface eth0
26
+ """,
27
+ )
28
+ parser.add_argument(
29
+ "--version", action="version", version=f"%(prog)s {__version__}"
30
+ )
31
+ parser.add_argument("mac", help="Target MAC address (AA:BB:CC:DD:EE:FF)")
32
+ parser.add_argument(
33
+ "--ip",
34
+ default="255.255.255.255",
35
+ help="Explicit broadcast/unicast IP (UDP modes, default: 255.255.255.255)",
36
+ )
37
+ parser.add_argument(
38
+ "--port", type=int, default=9, help="UDP port (default: 9)"
39
+ )
40
+ parser.add_argument(
41
+ "--network",
42
+ metavar="CIDR",
43
+ help="VLAN subnet in CIDR notation - derives directed broadcast IP (e.g. 192.168.10.0/24)",
44
+ )
45
+ parser.add_argument(
46
+ "--vlan-id",
47
+ type=int,
48
+ metavar="ID",
49
+ help="802.1Q VLAN ID 1-4094 - sends tagged raw Ethernet frame (requires --interface, root)",
50
+ )
51
+ parser.add_argument(
52
+ "--interface",
53
+ metavar="IFACE",
54
+ help="Egress network interface for raw 802.1Q mode (e.g. eth0)",
55
+ )
56
+ return parser
57
+
58
+
59
+ def main(argv: list[str] | None = None) -> int:
60
+ """Main entry point for the CLI."""
61
+ parser = create_parser()
62
+ args = parser.parse_args(argv)
63
+
64
+ try:
65
+ if args.vlan_id is not None:
66
+ # Mode 3: raw 802.1Q
67
+ if not args.interface:
68
+ parser.error("--vlan-id requires --interface")
69
+ send_dot1q(args.mac, args.vlan_id, args.interface, args.port)
70
+
71
+ elif args.network:
72
+ # Mode 2: directed broadcast via CIDR
73
+ bcast = broadcast_from_network(args.network)
74
+ print(f"[INFO] Directed broadcast for {args.network} -> {bcast}")
75
+ send_udp(args.mac, bcast, args.port)
76
+
77
+ else:
78
+ # Mode 1: plain UDP broadcast
79
+ send_udp(args.mac, args.ip, args.port)
80
+
81
+ return 0
82
+
83
+ except PermissionError:
84
+ print("[ERROR] Raw socket requires root privileges. Re-run with sudo.", file=sys.stderr)
85
+ return 1
86
+ except Exception as e:
87
+ print(f"[ERROR] {e}", file=sys.stderr)
88
+ return 1
89
+
90
+
91
+ if __name__ == "__main__":
92
+ sys.exit(main())
wol_cli/packet.py ADDED
@@ -0,0 +1,35 @@
1
+ """Magic packet construction for Wake-on-LAN."""
2
+
3
+ import re
4
+ import struct
5
+
6
+
7
+ def parse_mac(mac: str) -> bytes:
8
+ """Parse a MAC address string into bytes.
9
+
10
+ Accepts formats: AA:BB:CC:DD:EE:FF, AA-BB-CC-DD-EE-FF, AABB.CCDD.EEFF
11
+ """
12
+ clean = re.sub(r"[:\-.]", "", mac).upper()
13
+ if len(clean) != 12 or not all(c in "0123456789ABCDEF" for c in clean):
14
+ raise ValueError(f"Invalid MAC address: {mac!r}")
15
+ return bytes.fromhex(clean)
16
+
17
+
18
+ def build_magic_packet(mac: str) -> bytes:
19
+ """Build a Wake-on-LAN magic packet for the given MAC address.
20
+
21
+ The magic packet consists of 6 bytes of 0xFF followed by the target
22
+ MAC address repeated 16 times.
23
+ """
24
+ mac_bytes = parse_mac(mac)
25
+ return b"\xff" * 6 + mac_bytes * 16
26
+
27
+
28
+ def checksum(data: bytes) -> int:
29
+ """Calculate IP/UDP checksum."""
30
+ if len(data) % 2:
31
+ data += b"\x00"
32
+ s: int = sum(struct.unpack(f"!{len(data)//2}H", data))
33
+ while s >> 16:
34
+ s = (s & 0xFFFF) + (s >> 16)
35
+ return ~s & 0xFFFF
wol_cli/py.typed ADDED
File without changes
wol_cli/transport.py ADDED
@@ -0,0 +1,130 @@
1
+ """Network transport methods for Wake-on-LAN packets."""
2
+
3
+ import ipaddress
4
+ import socket
5
+ import struct
6
+ import sys
7
+
8
+ from .packet import build_magic_packet, checksum, parse_mac
9
+
10
+ ETH_P_8021Q = 0x8100
11
+ ETH_P_IP = 0x0800
12
+ IPPROTO_UDP = 17
13
+
14
+
15
+ def send_udp(mac: str, ip: str = "255.255.255.255", port: int = 9) -> None:
16
+ """Send a magic packet via UDP broadcast.
17
+
18
+ Args:
19
+ mac: Target MAC address
20
+ ip: Broadcast IP address (default: 255.255.255.255)
21
+ port: UDP port (default: 9)
22
+ """
23
+ packet = build_magic_packet(mac)
24
+ with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
25
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
26
+ sock.connect((ip, port))
27
+ sock.send(packet)
28
+ print(f"[UDP] Magic packet sent to {mac} via {ip}:{port}")
29
+
30
+
31
+ def broadcast_from_network(cidr: str) -> str:
32
+ """Compute the broadcast address for a given CIDR network."""
33
+ net = ipaddress.IPv4Network(cidr, strict=False)
34
+ return str(net.broadcast_address)
35
+
36
+
37
+ def build_udp_payload(magic: bytes, dst_ip: str, port: int) -> bytes:
38
+ """Wrap magic packet in a minimal IPv4/UDP frame (no IP options)."""
39
+ src_ip_bytes = socket.inet_aton("0.0.0.0")
40
+ dst_ip_bytes = socket.inet_aton(dst_ip)
41
+
42
+ udp_len = 8 + len(magic)
43
+ # UDP pseudo-header for checksum
44
+ pseudo = src_ip_bytes + dst_ip_bytes + struct.pack("!BBH", 0, IPPROTO_UDP, udp_len)
45
+ udp_header_no_cs = struct.pack("!HHHH", port, port, udp_len, 0)
46
+ udp_cs = checksum(pseudo + udp_header_no_cs + magic)
47
+ udp_header = struct.pack("!HHHH", port, port, udp_len, udp_cs)
48
+
49
+ ip_len = 20 + udp_len
50
+ ip_header_no_cs = struct.pack(
51
+ "!BBHHHBBH4s4s",
52
+ 0x45, 0, # version+IHL, DSCP/ECN
53
+ ip_len, # total length
54
+ 0, 0, # identification, flags+fragment offset
55
+ 64, IPPROTO_UDP, # TTL, protocol
56
+ 0, # checksum placeholder
57
+ src_ip_bytes,
58
+ dst_ip_bytes,
59
+ )
60
+ ip_cs = checksum(ip_header_no_cs)
61
+ ip_header = struct.pack(
62
+ "!BBHHHBBH4s4s",
63
+ 0x45, 0, ip_len, 0, 0, 64, IPPROTO_UDP,
64
+ ip_cs, src_ip_bytes, dst_ip_bytes,
65
+ )
66
+
67
+ return ip_header + udp_header + magic
68
+
69
+
70
+ def build_dot1q_frame(
71
+ magic: bytes,
72
+ src_mac: bytes,
73
+ dst_mac: bytes,
74
+ vlan_id: int,
75
+ dst_ip: str,
76
+ port: int,
77
+ ) -> bytes:
78
+ """Build a complete 802.1Q tagged Ethernet frame."""
79
+ if not (0 < vlan_id < 4095):
80
+ raise ValueError(f"VLAN ID must be 1-4094, got {vlan_id}")
81
+
82
+ tci = vlan_id & 0x0FFF # PCP=0, DEI=0, VID=vlan_id
83
+ payload = build_udp_payload(magic, dst_ip, port)
84
+
85
+ return (
86
+ dst_mac
87
+ + src_mac
88
+ + struct.pack("!HHH", ETH_P_8021Q, tci, ETH_P_IP)
89
+ + payload
90
+ )
91
+
92
+
93
+ def get_iface_mac(iface: str) -> bytes:
94
+ """Read the hardware MAC of a local interface (Linux /sys)."""
95
+ try:
96
+ path = f"/sys/class/net/{iface}/address"
97
+ with open(path) as f:
98
+ return parse_mac(f.read().strip())
99
+ except FileNotFoundError:
100
+ raise RuntimeError(f"Interface '{iface}' not found (or not on Linux)")
101
+
102
+
103
+ def send_dot1q(mac: str, vlan_id: int, iface: str, port: int = 9) -> None:
104
+ """Send a magic packet via 802.1Q tagged raw Ethernet frame.
105
+
106
+ This mode requires Linux and root privileges.
107
+
108
+ Args:
109
+ mac: Target MAC address
110
+ vlan_id: VLAN ID (1-4094)
111
+ iface: Network interface name (e.g., eth0)
112
+ port: UDP port (default: 9)
113
+ """
114
+ if sys.platform != "linux":
115
+ raise RuntimeError("802.1Q raw socket mode is only supported on Linux")
116
+
117
+ magic = build_magic_packet(mac)
118
+ dst_mac = bytes.fromhex("ffffffffffff") # Ethernet broadcast
119
+ src_mac = get_iface_mac(iface)
120
+ dst_ip = "255.255.255.255"
121
+
122
+ frame = build_dot1q_frame(magic, src_mac, dst_mac, vlan_id, dst_ip, port)
123
+
124
+ # ETH_P_ALL = 0x0003 -> send raw frames
125
+ ETH_P_ALL = 3
126
+ with socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(ETH_P_ALL)) as sock:
127
+ sock.bind((iface, 0))
128
+ sock.send(frame)
129
+
130
+ print(f"[802.1Q] Magic packet sent to {mac} on VLAN {vlan_id} via {iface}")
@@ -0,0 +1,183 @@
1
+ Metadata-Version: 2.4
2
+ Name: wol-cli
3
+ Version: 0.1.0
4
+ Summary: Wake-on-LAN magic packet sender with VLAN support
5
+ Project-URL: Homepage, https://github.com/yourusername/wol-cli-tool
6
+ Project-URL: Repository, https://github.com/yourusername/wol-cli-tool
7
+ Project-URL: Issues, https://github.com/yourusername/wol-cli-tool/issues
8
+ Project-URL: Changelog, https://github.com/yourusername/wol-cli-tool/releases
9
+ Author-email: Your Name <you@example.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: magic-packet,network,vlan,wake-on-lan,wol
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: System Administrators
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: System :: Networking
24
+ Classifier: Topic :: Utilities
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.10
27
+ Provides-Extra: dev
28
+ Requires-Dist: build>=1.4.3; extra == 'dev'
29
+ Requires-Dist: mypy>=1.20.0; extra == 'dev'
30
+ Requires-Dist: pyinstaller>=6.19.0; extra == 'dev'
31
+ Requires-Dist: pytest>=9.0.3; extra == 'dev'
32
+ Requires-Dist: ruff>=0.15.10; extra == 'dev'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # wol-cli
36
+
37
+ [![CI](https://github.com/yourusername/wol-cli-tool/actions/workflows/ci.yml/badge.svg)](https://github.com/yourusername/wol-cli-tool/actions/workflows/ci.yml)
38
+ [![PyPI version](https://badge.fury.io/py/wol-cli.svg)](https://badge.fury.io/py/wol-cli)
39
+ [![Python versions](https://img.shields.io/pypi/pyversions/wol-cli.svg)](https://pypi.org/project/wol-cli/)
40
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
41
+
42
+ A Wake-on-LAN magic packet sender with VLAN support.
43
+
44
+ ## Features
45
+
46
+ - **UDP Broadcast**: Standard Wake-on-LAN via UDP broadcast
47
+ - **Directed Broadcast**: Auto-compute broadcast address from CIDR notation
48
+ - **802.1Q VLAN Support**: Send tagged Ethernet frames directly (Linux only, requires root)
49
+
50
+ ## Installation
51
+
52
+ ### From PyPI
53
+
54
+ ```bash
55
+ pip install wol-cli
56
+ ```
57
+
58
+ ### From GitHub Releases (standalone binary)
59
+
60
+ Download the appropriate binary for your platform from the [Releases](https://github.com/yourusername/wol-cli-tool/releases) page:
61
+
62
+ - `wol-linux-amd64` - Linux x86_64
63
+ - `wol-windows-amd64.exe` - Windows x86_64
64
+ - `wol-macos-amd64` - macOS x86_64
65
+
66
+ ### From source
67
+
68
+ ```bash
69
+ pip install .
70
+ ```
71
+
72
+ For development:
73
+
74
+ ```bash
75
+ pip install -e ".[dev]"
76
+ ```
77
+
78
+ ## Usage
79
+
80
+ ### Plain UDP Broadcast (same subnet)
81
+
82
+ ```bash
83
+ wol AA:BB:CC:DD:EE:FF
84
+ ```
85
+
86
+ ### Directed Broadcast
87
+
88
+ Auto-computes the broadcast address from a CIDR network:
89
+
90
+ ```bash
91
+ wol AA:BB:CC:DD:EE:FF --network 192.168.10.0/24
92
+ ```
93
+
94
+ ### 802.1Q Tagged Frame (Linux only)
95
+
96
+ Send a VLAN-tagged raw Ethernet frame (requires root):
97
+
98
+ ```bash
99
+ sudo wol AA:BB:CC:DD:EE:FF --vlan-id 10 --interface eth0
100
+ ```
101
+
102
+ ### Options
103
+
104
+ | Option | Description |
105
+ |--------|-------------|
106
+ | `--ip IP` | Explicit broadcast/unicast IP (default: 255.255.255.255) |
107
+ | `--port PORT` | UDP port (default: 9) |
108
+ | `--network CIDR` | Subnet for directed broadcast (e.g., 192.168.10.0/24) |
109
+ | `--vlan-id ID` | 802.1Q VLAN ID (1-4094) |
110
+ | `--interface IFACE` | Network interface for 802.1Q mode |
111
+ | `--version` | Show version |
112
+
113
+ ## Library Usage
114
+
115
+ ```python
116
+ from wol_cli import send_udp, send_dot1q, build_magic_packet
117
+
118
+ # Send via UDP broadcast
119
+ send_udp("AA:BB:CC:DD:EE:FF")
120
+
121
+ # Send via directed broadcast
122
+ send_udp("AA:BB:CC:DD:EE:FF", ip="192.168.10.255")
123
+
124
+ # Build a magic packet manually
125
+ packet = build_magic_packet("AA:BB:CC:DD:EE:FF")
126
+ ```
127
+
128
+ ## Development
129
+
130
+ Install development dependencies:
131
+
132
+ ```bash
133
+ pip install -e ".[dev]"
134
+ ```
135
+
136
+ Run tests:
137
+
138
+ ```bash
139
+ pytest -v
140
+ ```
141
+
142
+ Run linter:
143
+
144
+ ```bash
145
+ ruff check src/ tests/
146
+ ```
147
+
148
+ Run type checker:
149
+
150
+ ```bash
151
+ mypy src/
152
+ ```
153
+
154
+ ## Releasing
155
+
156
+ Releases are automated via GitHub Actions. To create a new release:
157
+
158
+ 1. Update the version in `pyproject.toml` and `src/wol_cli/__init__.py`
159
+ 2. Commit the changes
160
+ 3. Create and push a tag:
161
+ ```bash
162
+ git tag v0.1.0
163
+ git push origin v0.1.0
164
+ ```
165
+
166
+ The workflow will automatically:
167
+ - Run tests
168
+ - Build binaries for Linux, Windows, and macOS
169
+ - Build the Python package
170
+ - Publish to PyPI
171
+ - Create a GitHub release with all artifacts
172
+
173
+ ### PyPI Setup
174
+
175
+ To enable PyPI publishing, add a `PYPI_API_TOKEN` secret to your repository:
176
+
177
+ 1. Go to [PyPI Account Settings](https://pypi.org/manage/account/token/)
178
+ 2. Create an API token
179
+ 3. Add it as a repository secret named `PYPI_API_TOKEN`
180
+
181
+ ## License
182
+
183
+ MIT
@@ -0,0 +1,11 @@
1
+ wol_cli/__init__.py,sha256=Rw-fw2stZBmSzK7SYbMmaz2oXI6kG49UMC5dejWnt9U,340
2
+ wol_cli/__main__.py,sha256=4XKkh05uFu3nM1s9a3rvk9_LnLoEVUEN77cIx-1MXfk,125
3
+ wol_cli/cli.py,sha256=5kEmrbc8bBElyk6r-Phf_Xlr9j8gOs0pm2EwhboOeJM,2855
4
+ wol_cli/packet.py,sha256=-QKUEjqqyI9JHnanMYJFEGtAZy242Q5svH2cF3hH9mM,1008
5
+ wol_cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ wol_cli/transport.py,sha256=-VJ7B6mYTkBRuieGA7zdi8vYTlCMP0guR6tR3VdGSnI,4111
7
+ wol_cli-0.1.0.dist-info/METADATA,sha256=XJvrXMS41Jxs_YnTMV4XsPyoDRyJAWqHdkuxrx6c-QI,4690
8
+ wol_cli-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
9
+ wol_cli-0.1.0.dist-info/entry_points.txt,sha256=nLHk6MW6XSWtr1_QRxWjOpfBqAg0v7Wa9shn2sq6lBY,41
10
+ wol_cli-0.1.0.dist-info/licenses/LICENSE,sha256=dUhuoK-TCRQMpuLEAdfme-qPSJI0TlcH9jlNxeg9_EQ,1056
11
+ wol_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ wol = wol_cli.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.