今日已更新 84 条资讯 | 累计 37635 条内容
关于我们

标签:#dev

找到 4368 篇相关文章

AI 资讯

Namecheap closes every auction at 11:00 AM ET. Last-second bidding is a myth.

If you have ever tried to win a domain at auction, you probably assumed the game works like eBay: watch the clock, wait for the last eight seconds, fire your bid, walk away with the name. On Namecheap, that does not work. Not "works badly". Does not work. Namecheap's expiring and marketplace auctions close in a daily batch at 11:00 AM ET. Every auction ending that day ends at roughly the same moment, which means there is no quiet corner of the day where you and one other bidder are paying attention. And if a bid lands in the closing window, the auction extends. So the buzzer-beater you were planning gets absorbed and the clock keeps running. The winner is not the fastest click. The winner is whoever set the smartest proxy maximum, on a name they found before anyone else was looking at it. I have been building PounceDomains around that one fact for months, and it is the reason the product looks the way it does. The edge moved from timing to discovery If speed is not the lever, the levers left are: find the good names earlier, and know what they are actually worth before you commit a number. So the engine scans the Namecheap aftermarket around the clock rather than at the bell. You describe the domains you want in plain English, something like "pronounceable 5-letter .com brandables under $50, no numbers or hyphens", and it builds a tuned config you can edit. If your config is too broad, it tells you and tightens it. There are seven scoring lenses you can stack: pronounceable, brandable, exact-match keyword, short premium, dictionary word, two-word combo, and free-text custom criteria. Fast programmatic filters run first, then AI scores what survives, and only domains that clear your threshold become matches. It has graded over 340,000 domains so far. The second lever is the one I care about more. Every match arrives with its receipts The failure mode in domain investing is not missing a name. It is paying $400 for something worth $80 because a free appraisal tool pri

2026-08-19 原文 →
AI 资讯

DNS Troubleshooting with dig: The Commands DevOps Engineers Actually Need

A surprising share of "the app is down" pages resolve to a name-resolution problem, not a broken service. The service is fine; the client can't turn a name into an address. dig is the precision tool for proving that in seconds instead of guessing. Think about it as a resolution chain, not "is DNS broken" When a name fails, work the chain: which resolver did the client ask, what did that resolver return, and does it match what authoritative DNS actually says? Most incidents live in the gap between those three. The method is boring and reliable: observe the symptom, form a hypothesis about where in the chain it breaks, test with one query, read the evidence, fix, then validate. The single most important habit: query the name from the same host and the same resolver the app uses. Running dig from your laptop proves nothing about what the pod or VM sees. The record types worth knowing You don't need all of them, but you need to recognize them: A / AAAA — name to IPv4 / IPv6 address. The usual suspect. CNAME — an alias pointing at another name. A stale or wrong CNAME sends traffic somewhere unexpected. MX — mail routing. TXT — SPF, DKIM, domain verification, and other metadata. NS — which servers are authoritative for a zone. SOA — the zone's serial and TTL defaults; the serial tells you whether a change has propagated. PTR — reverse lookup, IP back to name. The commands that actually earn their place Start with the quick answer, then get precise. dig +short api.internal.example.com +short strips everything except the answer. If it prints an IP, resolution works from this host. If it prints nothing, you have a real failure to chase. Empty output is a signal, not an error. dig api.internal.example.com A The full form. Read the status in the header: NOERROR with an ANSWER section is good; NXDOMAIN means the name genuinely doesn't exist; SERVFAIL points at a broken upstream or DNSSEC issue. Also note which SERVER answered at the bottom — that's the resolver you're actually

2026-08-19 原文 →
AI 资讯

GitHub API Rate Limits: an Unauthenticated 304 Still Costs You a Request

No token. One IP. July 29, 2026: GET /repos/python/cpython 200 5996 B remaining 32 -> 31 + If-None-Match (no Authorization header) 304 0 B remaining 31 -> 30 + If-None-Match 304 0 B remaining 30 -> 29 + If-None-Match 304 0 B remaining 29 -> 28 Three conditional requests. Three 304 Not Modified . Zero bytes of body across all three. Three requests gone from a bucket of 60 per hour. I opened the terminal to write the opposite post. The short version: if you call the GitHub REST API without an Authorization header, an If-None-Match request that comes back 304 still decrements x-ratelimit-remaining . The ETag saves you bytes. It does not save you quota. GitHub's documentation states the claim five times on one page and attaches the condition to two of them, and that clause falls off easily when a sentence gets quoted on its own. The post I meant to write My working title was something like "poll GitHub for free with ETags". I believed it. I had read the sentence about 304 responses not using your rate limit, I had repeated it to other people, and the plan was a tidy little piece with a before-and-after budget chart. The first run killed it. remaining went down. My first reaction was that my counter reading was wrong, which is the normal reaction and usually the correct one. It was not wrong. So the post changed, and the finding turned out to be worth more than the one I went in with. Does a 304 count against the GitHub rate limit? What the docs actually say Here is the part that matters, and I want to be precise because it would be easy and dishonest to turn this into "GitHub's docs are wrong". They are not. On the page Best practices for using the REST API the claim shows up five times. Two of the five carry a condition; three do not. Here is the strict one, the only place on the page where the condition is spelled out as a header: "Making a conditional request does not count against your primary rate limit if a 304 response is returned and the request was made while c

2026-08-19 原文 →
AI 资讯

I'm building Guren, a fullstack TypeScript framework for the AI-agent era

Guren is a fullstack TypeScript framework for Bun. I started it because I wanted Laravel's shape in TypeScript, and I kept going for a different reason: once I was handing most of the code to agents, what I wanted from a framework was a way to check what came back. gurenjs / guren Guren is a Bun-native TypeScript MVC framework that unites Laravel-like ergonomics with Hono, Inertia.js, React, and Drizzle ORM, aiming to deliver a fast, elegant full-stack workflow that keeps frontend and backend work in sync. Guren The fullstack TypeScript framework for the AI-agent era. Laravel-style conventions, end-to-end type safety, and built-in agent introspection and verification — routing, controllers, ORM, authentication, and Inertia.js + React in one cohesive experience that humans and AI coding agents navigate from the same map. v2 — Stable. Breaking changes only in major releases, per the release policy . Quick Start # 1. Scaffold a new app with authentication (dependencies install automatically) bunx create-guren-app my-app --auth cd my-app # 2. Run migrations and seed the demo user (SQLite by default — no server needed) bun run db:migrate bun run db:seed # 3. Start the dev server bun run dev Open http://localhost:3333 and sign in at /login with demo@example.com / secret . Add features as you go bunx guren add auth # Authentication bunx guren add resource posts --fields " title:string,body:text " # CRUD resource bunx guren add queue # Background jobs … View on GitHub I like the way Laravel and Rails let you build. A feature is a route, a controller, a model and a view, and authentication, queues, mail and validation are already wired together before you start. TypeScript has the parts. Hono for HTTP, Drizzle for the ORM, Zod for validation, Inertia and React for rendering, all of them good. What's missing is an agreed way to connect them, so every project ends up wiring it slightly differently, and I've written that wiring more times than I want to count. The mistakes move

2026-08-19 原文 →
AI 资讯

I Built a 40-Minute Evaluation for Free Model Endpoints. Here's the Scorecard.

Free model endpoints are seductive. Zero cost. Zero setup. Zero reason to trust them. I don't trust demos. I trust failure modes. So I built a small evaluation harness. It tests one thing: can a free model endpoint gate a pull request for secrets? This is not a benchmark. It's a repeatable experiment. You can run it in an afternoon. Disclosure: This article was prepared as part of MonkeyCode's product outreach. I used MonkeyCode's free model endpoint and the free server option for the test. No quotas. No hardware claims. Just a harness and a rubric. Why I stopped trusting free endpoints Free endpoints look great in a demo. You paste a diff. The model finds the secret. Everyone claps. Then you wire it into CI. The JSON breaks. The latency spikes. The model misses a private key. The demo didn't show that. An evaluation will. The experiment I designed a 40-minute test. It answers one question: where does the free endpoint perform well, and where does it break? The dataset is 30 synthetic diffs. Fifteen contain real-looking secrets. Fifteen are clean. Each diff is small. Each diff has one clear change. The prompt is strict. The model must return JSON. No prose. No apologies. Just a verdict. # eval_secret_gate.py # Simplified harness. Adapt to your client SDK. import json , time def classify ( client , diff : str ) -> dict : prompt = f """ You are a secret scanner for code review. Return ONLY JSON with this shape: {{ " contains_secret " : true, " line " : 12, " type " : " aws_access_key " }} Diff: { diff } """ start = time . time () response = client . complete ( prompt , model = " free " , server = " free " , # free server option ) latency = time . time () - start return { " latency " : latency , " raw " : response } def evaluate ( client , diffs , runs = 3 ): for i , diff in enumerate ( diffs ): for run in range ( runs ): yield i , run , classify ( client , diff ) The harness is deliberately small. It measures five things. Accuracy. JSON validity. Latency. Variance. Fa

2026-08-19 原文 →
AI 资讯

Opinion: The Diff Is a Claim, the Probe Is the Proof

Opinion: The Diff Is a Claim, the Probe Is the Proof A generated patch is a claim about how a system should behave, and a diff cannot verify that claim on its own. The only honest reviewer is the runtime itself, which means every AI-proposed change deserves a behavioral probe before a human spends attention on it. Free model access changes the economics of that review, because the verification loop no longer costs a developer's full attention or a paid compute budget. The practical implication is that a disposable server, such as the free server option in MonkeyCode, becomes the arbiter of whether a patch is even worth reading. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Review sessions routinely burn forty minutes on a diff that a five-second HTTP probe would have rejected instantly, and that waste is now entirely avoidable. Why Line-by-Line Review Fails on AI Patches A human reviewer reads a diff as prose, searching for the author's intent, but an AI-generated patch has no reliable intent to recover. The model that wrote the change cannot explain why a specific flag was flipped, and the diff itself only records the surface edit. This is a fundamental mismatch between the review tool and the review question. The review question is not "what changed" but "does the system still behave correctly after this change." Runtime shape diffing answers the first question well, and I have argued before that shape is a useful gate, but shape alone misses semantic regressions. A service can keep the same endpoints, the same config keys, and the same file layout while silently returning wrong data. Behavioral probes close that gap because they test the contract between the service and its callers. A probe sends real requests, checks real responses, and records real state transitions, which is exactly the evidence a reviewer needs. This is why I take the position that the probe, not the diff, should be the primary review artifact. Treat Every Pa

2026-08-19 原文 →
AI 资讯

The Login Loop of Doom.

This is a submission for DEV's Summer Bug Smash: Smash Stories powered by Sentry . Code snippets are recreated and anonymized for illustrative purposes. The Symptom: A Revolving Door Instead of a Login Page It started innocently enough: I was clicking through our app and hit "Log in." Auth0's Universal Login page appeared, I entered my credentials, got redirected back to the app... and landed on the Auth0 login page again. And again. And again. No error message. No failed login attempt. Auth0 was happily authenticating me every single time — and our app was just as happily bouncing me right back, like a bouncer who checks your ID, nods, and then immediately forgets he checked it. The login loop. Every developer's favorite horror movie, now starring me. Red Herring #1: "It's the Frontend's Fault" My first suspect was the obvious one: the frontend callback handler. A Node.js/Express app sits in front of our Django API, handling the Auth0 redirect dance. A login loop screams "broken callback" or "state/nonce mismatch," so I spent a solid hour there: ✅ State parameter matched ✅ Nonce validated ✅ Callback URL whitelisted in the Auth0 dashboard ✅ ID token and access token both present in the response Everything the frontend touched was perfect. The tokens were real, signed by Auth0, freshly issued seconds ago. And yet the moment the frontend sent the access token to our Django API, the API answered with a flat 401 Unauthorized . Fine. New suspect. Red Herring #2: "Auth0 Must Be Misconfigured" Next stop: the Auth0 dashboard. Maybe the token lifetime was set to something absurd, like 5 seconds? Maybe the audience claim was wrong? Token lifetime: 3600 seconds. Normal. aud claim: matched our API identifier exactly. Signature: verified against the JWKS. Valid. So Auth0 was issuing perfectly good tokens, the frontend was delivering them intact, and Django was spitting them out. The bug had to be in the validation logic itself. Time to actually read the code we trusted blindly e

2026-08-19 原文 →
AI 资讯

What If the Blockchain Could Judge Your Bluff Without Seeing Your Dice?

Liar’s Dice sounds like a perfect game to put onchain. The rules are simple, every move can be verified, and you don’t need a centralized game server deciding who won. There is just one problem. Blockchains are public. Liar’s Dice only works if your dice are private. If I simply stored every roll inside a normal smart contract, anyone could inspect the state and know exactly what everyone was holding. At that point, there is no bluffing. You would basically be playing poker with everyone's cards face up. So I built FHE Liar’s Dice , a decentralized version of the game where your dice remain encrypted while the game is being played. Not hidden behind a backend. Not stored privately in some database. Encrypted onchain. And the interesting part is that the smart contract can still use those encrypted dice to determine whether you are lying. The problem with putting hidden-information games onchain Most blockchain games actually benefit from transparency. If you're building something like chess, every player is supposed to know the complete state of the board. Liar’s Dice is different. Each player starts with five dice that only they should be able to see. Players then make public claims about the combined dice across the entire table. You might say: There are six 4s on the table. The next player has two choices. Raise the bid. Or call your bluff. The entire game comes from the fact that nobody knows exactly what everyone else is holding. But a traditional smart contract has the opposite property. Its state is transparent. Even if the frontend refuses to display your dice, someone can simply inspect the contract, query the state, watch events, or build their own interface. Hiding something in the UI isn't privacy. I needed the actual game state itself to remain secret. FHE turned out to be a very good fit for the game I built the game using Fhenix CoFHE . Fully Homomorphic Encryption is interesting because it allows computation to happen directly over encrypted values.

2026-08-19 原文 →
AI 资讯

React useScrollLock Hook: Lock Body Scroll for Modals (2026)

Your modal is open, centered, perfect. Then someone flicks the overlay and the page behind it scrolls away underneath. Everyone's first fix is the same three lines: useEffect (() => { document . body . style . overflow = open ? " hidden " : "" ; }, [ open ]); It works on your laptop. Then the bug reports arrive: On iPhone the page still moves. iOS Safari rubber-band scrolls the document by touch even with overflow: hidden on <body> . Something else got wiped. "" isn't necessarily what was there before — you just erased whatever your design system or CSS-in-JS had set inline. Two overlays, one frozen page. A drawer and a lightbox both own body.style.overflow ; close them in the wrong order and the page never scrolls again. The layout jumps the instant the desktop scrollbar disappears. useScrollLock from @reactuses/core is those three lines with the hard parts handled: it restores the exact inline overflow it replaced, adds a touchmove guard on iOS that still lets your modal's own content scroll, exposes the lock as React state you can render off, and works on any element — not just <body> . This post covers what it actually does line by line, why overflow: hidden is not enough on iOS, how it compares to the position: fixed and body:has(dialog[open]) approaches, and the six gotchas that show up in real apps. Quick Start npm install @reactuses/core import { useScrollLock } from " @reactuses/core " ; import { useEffect } from " react " ; function Modal ({ open , onClose , children }: ModalProps ) { // a getter, not `document.body` — see the SSR gotcha below const [, setLocked ] = useScrollLock (() => document . body ); useEffect (() => { setLocked ( open ); return () => setLocked ( false ); // release even if we unmount while open }, [ open , setLocked ]); if ( ! open ) return null ; return ( < div className = "overlay" onClick = { onClose } > < div className = "sheet" onClick = { e => e . stopPropagation () } > { children } </ div > </ div > ); } The signature: const [

2026-08-19 原文 →
AI 资讯

Why pasted text keeps breaking search and formatting (and the regexes I ended up using to clean it)

I kept running into a boring problem that was harder to debug than it should have been: text that looked normal, but behaved wrong the moment I pasted it into a CMS, a spreadsheet, or a code comment. Search would fail. Line breaks would get weird. A heading copied from ChatGPT would drag Markdown markers along with it. Sometimes the only visible clue was that the punctuation felt slightly "off." What finally made this manageable wasn't some big NLP trick. It was going back to the dumb, reliable layer: exact character matching. The tool I built for this is basically a pile of small, deterministic cleanups for the specific junk that copied text tends to accumulate — full-width punctuation mixed into ASCII, invisible Unicode code points, curly quotes, em dashes, leftover Markdown, and whitespace noise. The most useful part is the invisible-character scan, not the cleaning The piece I trust most in the whole component is the part that explicitly names which invisible characters it cares about, then counts them by code point. It's not doing a vague "this text seems suspicious" pass. It has a hard-coded inventory: const invisibleDefs = [ { key : " zwsp " , codes : [ 0x200b ] }, { key : " zwnj " , codes : [ 0x200c ] }, { key : " zwj " , codes : [ 0x200d ] }, { key : " bomZwnbsp " , codes : [ 0xfeff ] }, { key : " wordJoiner " , codes : [ 0x2060 ] }, { key : " softHyphen " , codes : [ 0x00ad ] }, { key : " bidiMarks " , codes : [ 0x200e , 0x200f , 0x202a , 0x202b , 0x202c , 0x202d , 0x202e ] }, ]; const codesToRegex = ( codes ) => new RegExp ( `[ ${ codes . map (( c ) => " \\ u " + c . toString ( 16 ). padStart ( 4 , " 0 " )). join ( "" )} ]` , " g " ); const analyzeInvisible = ( str ) => { const breakdown = invisibleDefs . map (( def ) => ({ key : def . key , count : ( str . match ( codesToRegex ( def . codes )) || []). length , })); const total = breakdown . reduce (( sum , row ) => sum + row . count , 0 ); return { breakdown , total }; }; I like this because it's brutall

2026-08-19 原文 →
AI 资讯

I was tired of clunky PGP tools, so i built my own cross-platform solution: PGP Manager

I work with PGP regularly and I work with it across multiple operating systems. Linux on my workstation, a MacBook on the go and every now and then I have to touch Windows. And on every single one of them, PGP means a different tool: Kleopatra on Linux, GPG Keychain on Mac, Gpg4win on Windows (which is Kleopatra again, just wrapped differently). Three tools, three UIs, three sets of quirks, three different workflows and none of them are what I'd call user-friendly. And yes, i know: the GnuPG CLI is the same everywhere, and it's a great tool. I use it. But gpg --encrypt --sign --armor -r test@key.com is not something I want to type 100 times a day and it's definitely not something I can give to a non-technical colleague. Every time I had to walk someone through encrypting or decrypting a message, I lost a bit of hope. So I made it my mission to finally build something better: PGP Manager . A free and open-source desktop app that looks and works the same on Linux, Mac and Windows. Why another PGP Tool? The cryptography behind OpenPGP is mature and has been trusted for decades. The problem was never the crypto, it's the workflow and the fragmentation. Encrypting a message for a colleague shouldn't require different tools per OS and a wiki page. My goal was simple: all the everyday PGP tasks in one place, without dumbing anything down or inventing a new format. PGP Manager is not a new crypto system. It uses gopenpgp v3 (ProtonMail's OpenPGP library) and standard OpenPGP (RFC 4880), so it stays fully compatible with GPG, Kleopatra, Thunderbird and the rest. You can leave anytime, your keys are just standard armored files. One more thing that sets it apart from the tools above: they're all frontends for a local GnuPG installation. PGP Manager brings its own OpenPGP implementation, so there's nothing else to install. It can still read an existing GnuPG keyring if you have one, but it doesn't need it. That's also what makes the standalone/USB mode possible in the first pla

2026-08-19 原文 →
开发者

Stop Writing Media Queries for Font Size

A teammate opened a PR titled "fix hero heading on small screens." The diff added a media query. Mine, reviewing it, found four more already in that file — one per breakpoint, added over eighteen months by four different people, each one patching the width the last person didn't think of: .hero-heading { font-size : 3rem ; } @media ( max-width : 1200px ) { .hero-heading { font-size : 2.5rem ; } } @media ( max-width : 992px ) { .hero-heading { font-size : 2.25rem ; } } @media ( max-width : 768px ) { .hero-heading { font-size : 1.75rem ; } } @media ( max-width : 480px ) { .hero-heading { font-size : 1.5rem ; } } Five rules to make one number — the font size of one heading — track the width of the screen it's on. And it still didn't work everywhere: resize the window to 850px and the heading is stuck at the 992px value, a little too big for the space it actually has. Every gap between breakpoints is a size nobody chose, it's just whatever the nearest rule left behind. Here's the part that stings: none of this has been necessary since 2020. The fix that isn't a breakpoint at all clamp() takes three values — a minimum, a preferred value, and a maximum — and returns whichever one the situation calls for: .hero-heading { font-size : clamp ( 1.5rem , 1rem + 2vw , 3rem ); } Read it as a sentence: never smaller than 1.5rem, never bigger than 3rem, and in between, scale with the viewport. The five media queries above collapse into that one line — and unlike them, it doesn't have gaps. clamp() recalculates the size continuously, every pixel the viewport moves, so there's no "850px value" that got left behind. It's a formula, not a lookup table. The middle value is where the "preferred" size lives, and it's 1rem + 2vw — a fixed part plus a viewport-relative part — not just 4vw on its own. That's not decoration. It's the one part of this pattern worth getting right, because the shortcut version quietly breaks something. The version that looks fine and isn't The formula you'll see

2026-08-19 原文 →
开发者

React Router v8: A Deliberately Boring Release with ESM-Only Builds and Default Middleware

React Router v8 was released on June 17, 2026, with minimal breaking changes and new baselines. Key updates include an ESM-only build and default middleware settings. React Router v6 and Remix v2 have reached End of Life. Developers should follow specific migration guidelines to update their applications, while some are considering alternatives like TanStack Router. By Daniel Curtis

2026-08-19 原文 →
AI 资讯

A 2-Token Prompt and a 39,966-Token Bill: Measuring What My Agent Actually Costs

There is a small cluster of posts going around right now about auditing your LLM invoice, and about how cost calculators get the numbers wrong. I went to check mine and hit a problem before I got to the arithmetic: my pipeline doesn't produce an invoice, and the plumbing I built two months ago is the reason why. This project has a script, git_commit.py , that turns a staged git diff into a Conventional Commit message. It shells out to the Claude CLI. There is no ANTHROPIC_API_KEY anywhere in the project, on purpose — an early version used urllib against the API directly and broke immediately for anyone running on an OAuth session instead of a raw key, so every AI call in the repo goes through a claude -p subprocess instead. That decision is still right. It also means there is no API key, so there is no per-key usage dashboard, so there is no line item to audit. For several months this script has been making a model call on essentially every commit, and I have never once known what any of them cost. The call site throws the numbers away Here is the actual invocation, trimmed: raw = subprocess . check_output ( [ " claude " , " -p " , " --safe-mode " , SYSTEM + " \n\n " + diff ], text = True , timeout = 20 , env = _claude_subprocess_env (), ) subprocess.check_output returns stdout. With the CLI's default output format, stdout is the commit message string and nothing else. Every number I would want — tokens in, tokens out, dollars — is computed on the other side of that call and then discarded, because I asked for a string and a string is what I got. This is the part I want to flag for anyone wiring up a headless model call the same way. It isn't that the metering is missing. It's that the default output format is lossy in exactly the dimension you'd later want to audit, and you won't discover that by reading your own code, because your own code looks fine. It asks for text, it gets text. The fix is one flag: raw = subprocess . check_output ( [ " claude " , " -p " , " -

2026-08-19 原文 →
AI 资讯

UFW and WireGuard: the tunnel is up and nothing goes through

The tunnel comes up. wg show prints a recent handshake. The client has its address inside the tunnel. And not a single byte reaches the internet. Almost every guide answers this with "open UDP 51820 in the firewall". You already did that — it is why the handshake works at all. The problem is somewhere else, and UFW makes the distinction easy to miss: Entering a machine and traversing it are two different permissions. ufw allow 51820/udp lets packets arrive at the server. Your clients' traffic does not stop there — it goes through the box and out the public interface. That path lives in the FORWARD chain, which UFW denies by default and which no allow rule touches. The four things to check, in order 1. IP forwarding — and the file that overwrites the other file This is the one that costs hours, because the setting looks done. UFW loads its own sysctl file at startup, and it takes precedence over the system one. A value you carefully set in /etc/sysctl.conf can be silently overwritten on the next ufw enable . The right place is /etc/ufw/sysctl.conf : net / ipv4 / ip_forward = 1 net / ipv6 / conf / default / forwarding = 1 net / ipv6 / conf / all / forwarding = 1 Then check the effective value, not the file you just edited: sysctl net.ipv4.ip_forward 2. Forwarding, which is not the same as ingress Targeted, and the one to prefer: sudo ufw route allow in on wg0 out on eth0 Or globally, in /etc/default/ufw : DEFAULT_FORWARD_POLICY = "ACCEPT" The second opens forwarding for every interface. It is a good ten-second diagnostic and a poor permanent configuration. 3. NAT, which UFW never adds on its own Without it, packets leave carrying their tunnel address, which nothing on the internet knows how to answer. In /etc/ufw/before.rules , at the very top , before the *filter line: *nat :POSTROUTING ACCEPT [0:0] -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE COMMIT Two classic mistakes here: putting this block after *filter (it is then ignored), and copying eth0 without chec

2026-08-19 原文 →